From dc12ca42e16552db574be4779745bedccf4ba93a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 03:48:26 +0000 Subject: [PATCH 1/4] Scaffold WordPress Remote Editor desktop app and companion plugin Tauri 2 app with a Rust core (REST client, Application Password sign-in, OS keychain storage, isolated editor webview running the site's own block editor), a platform-adaptive React shell, the Remote Editor Bridge plugin, and CI/release workflows producing .dmg, .deb and .msi installers. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_013wWY5SvB2w46Qw92qtHG5e --- .github/workflows/ci.yml | 69 + .github/workflows/release.yml | 82 + README.md | 79 +- docs/ARCHITECTURE.md | 118 + docs/SECURITY.md | 54 + index.html | 13 + package-lock.json | 3233 +++++++++ package.json | 35 + .../remote-editor-bridge/assets/embedded.css | 21 + plugin/remote-editor-bridge/composer.json | 21 + plugin/remote-editor-bridge/composer.lock | 437 ++ .../includes/class-embedded-mode.php | 65 + .../includes/class-handoff.php | 231 + plugin/remote-editor-bridge/phpcs.xml.dist | 24 + plugin/remote-editor-bridge/readme.txt | 34 + .../remote-editor-bridge.php | 29 + plugin/remote-editor-bridge/uninstall.php | 19 + src-tauri/Cargo.lock | 5954 +++++++++++++++++ src-tauri/Cargo.toml | 45 + src-tauri/build.rs | 23 + src-tauri/capabilities/main.json | 31 + src-tauri/entitlements.plist | 11 + src-tauri/icons/128x128.png | Bin 0 -> 5894 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 10551 bytes src-tauri/icons/32x32.png | Bin 0 -> 1753 bytes src-tauri/icons/64x64.png | Bin 0 -> 3170 bytes src-tauri/icons/Square107x107Logo.png | Bin 0 -> 5043 bytes src-tauri/icons/Square142x142Logo.png | Bin 0 -> 6379 bytes src-tauri/icons/Square150x150Logo.png | Bin 0 -> 6715 bytes src-tauri/icons/Square284x284Logo.png | Bin 0 -> 11361 bytes src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1608 bytes src-tauri/icons/Square310x310Logo.png | Bin 0 -> 12118 bytes src-tauri/icons/Square44x44Logo.png | Bin 0 -> 2272 bytes src-tauri/icons/Square71x71Logo.png | Bin 0 -> 3639 bytes src-tauri/icons/Square89x89Logo.png | Bin 0 -> 4384 bytes src-tauri/icons/StoreLogo.png | Bin 0 -> 2611 bytes src-tauri/icons/icon.icns | Bin 0 -> 84489 bytes src-tauri/icons/icon.ico | Bin 0 -> 21160 bytes src-tauri/icons/icon.png | Bin 0 -> 19338 bytes .../autogenerated/add_site_manual.toml | 11 + .../autogenerated/begin_authorization.toml | 11 + .../autogenerated/close_editor.toml | 11 + .../autogenerated/discover_site.toml | 11 + .../autogenerated/editor_command.toml | 11 + .../autogenerated/editor_report.toml | 11 + .../autogenerated/editor_set_bounds.toml | 11 + .../autogenerated/get_site_context.toml | 11 + .../permissions/autogenerated/list_posts.toml | 11 + .../permissions/autogenerated/list_sites.toml | 11 + .../autogenerated/open_editor.toml | 11 + .../autogenerated/remove_site.toml | 11 + .../permissions/autogenerated/trash_post.toml | 11 + src-tauri/src/auth.rs | 254 + src-tauri/src/commands.rs | 256 + src-tauri/src/editor.rs | 410 ++ src-tauri/src/editor_bridge.js | 163 + src-tauri/src/error.rs | 69 + src-tauri/src/keychain.rs | 100 + src-tauri/src/lib.rs | 61 + src-tauri/src/main.rs | 6 + src-tauri/src/menu.rs | 96 + src-tauri/src/security.rs | 122 + src-tauri/src/sites.rs | 166 + src-tauri/src/wp/client.rs | 242 + src-tauri/src/wp/discovery.rs | 136 + src-tauri/src/wp/mod.rs | 117 + src-tauri/src/wp/models.rs | 242 + src-tauri/tauri.conf.json | 107 + src-tauri/tauri.macos.conf.json | 30 + src-tauri/tauri.windows.conf.json | 19 + src/App.tsx | 262 + src/components/AddSiteSheet.tsx | 202 + src/components/EditorView.tsx | 145 + src/components/Icon.tsx | 42 + src/components/PostList.tsx | 207 + src/components/Sidebar.tsx | 97 + src/components/SiteAvatar.tsx | 40 + src/lib/api.ts | 127 + src/lib/format.test.ts | 27 + src/lib/format.ts | 49 + src/lib/platform.ts | 20 + src/main.tsx | 20 + src/styles/app.css | 862 +++ tsconfig.json | 21 + vite.config.ts | 23 + 85 files changed, 15510 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/SECURITY.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 plugin/remote-editor-bridge/assets/embedded.css create mode 100644 plugin/remote-editor-bridge/composer.json create mode 100644 plugin/remote-editor-bridge/composer.lock create mode 100644 plugin/remote-editor-bridge/includes/class-embedded-mode.php create mode 100644 plugin/remote-editor-bridge/includes/class-handoff.php create mode 100644 plugin/remote-editor-bridge/phpcs.xml.dist create mode 100644 plugin/remote-editor-bridge/readme.txt create mode 100644 plugin/remote-editor-bridge/remote-editor-bridge.php create mode 100644 plugin/remote-editor-bridge/uninstall.php create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/main.json create mode 100644 src-tauri/entitlements.plist create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/64x64.png create mode 100644 src-tauri/icons/Square107x107Logo.png create mode 100644 src-tauri/icons/Square142x142Logo.png create mode 100644 src-tauri/icons/Square150x150Logo.png create mode 100644 src-tauri/icons/Square284x284Logo.png create mode 100644 src-tauri/icons/Square30x30Logo.png create mode 100644 src-tauri/icons/Square310x310Logo.png create mode 100644 src-tauri/icons/Square44x44Logo.png create mode 100644 src-tauri/icons/Square71x71Logo.png create mode 100644 src-tauri/icons/Square89x89Logo.png create mode 100644 src-tauri/icons/StoreLogo.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/permissions/autogenerated/add_site_manual.toml create mode 100644 src-tauri/permissions/autogenerated/begin_authorization.toml create mode 100644 src-tauri/permissions/autogenerated/close_editor.toml create mode 100644 src-tauri/permissions/autogenerated/discover_site.toml create mode 100644 src-tauri/permissions/autogenerated/editor_command.toml create mode 100644 src-tauri/permissions/autogenerated/editor_report.toml create mode 100644 src-tauri/permissions/autogenerated/editor_set_bounds.toml create mode 100644 src-tauri/permissions/autogenerated/get_site_context.toml create mode 100644 src-tauri/permissions/autogenerated/list_posts.toml create mode 100644 src-tauri/permissions/autogenerated/list_sites.toml create mode 100644 src-tauri/permissions/autogenerated/open_editor.toml create mode 100644 src-tauri/permissions/autogenerated/remove_site.toml create mode 100644 src-tauri/permissions/autogenerated/trash_post.toml create mode 100644 src-tauri/src/auth.rs create mode 100644 src-tauri/src/commands.rs create mode 100644 src-tauri/src/editor.rs create mode 100644 src-tauri/src/editor_bridge.js create mode 100644 src-tauri/src/error.rs create mode 100644 src-tauri/src/keychain.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/src/menu.rs create mode 100644 src-tauri/src/security.rs create mode 100644 src-tauri/src/sites.rs create mode 100644 src-tauri/src/wp/client.rs create mode 100644 src-tauri/src/wp/discovery.rs create mode 100644 src-tauri/src/wp/mod.rs create mode 100644 src-tauri/src/wp/models.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src-tauri/tauri.macos.conf.json create mode 100644 src-tauri/tauri.windows.conf.json create mode 100644 src/App.tsx create mode 100644 src/components/AddSiteSheet.tsx create mode 100644 src/components/EditorView.tsx create mode 100644 src/components/Icon.tsx create mode 100644 src/components/PostList.tsx create mode 100644 src/components/Sidebar.tsx create mode 100644 src/components/SiteAvatar.tsx create mode 100644 src/lib/api.ts create mode 100644 src/lib/format.test.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/platform.ts create mode 100644 src/main.tsx create mode 100644 src/styles/app.css create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb89fbb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,69 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build + + rust: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci && npm run build + - run: cargo fmt --check + working-directory: src-tauri + - run: cargo clippy --all-targets -- -D warnings + working-directory: src-tauri + - run: cargo test + working-directory: src-tauri + + plugin: + runs-on: ubuntu-latest + defaults: + run: + working-directory: plugin/remote-editor-bridge + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + tools: composer + - run: composer install --no-interaction --no-progress + - run: find . -path ./vendor -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l + - run: vendor/bin/phpcs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0e1860 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Release + +# Builds signed installers on real macOS, Linux and Windows machines when a +# version tag is pushed, and attaches them to a draft GitHub release. +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + args: --target universal-apple-darwin --bundles app,dmg + - platform: ubuntu-22.04 + args: --bundles deb,appimage + - platform: windows-latest + args: --bundles msi + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - name: Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ runner.os == 'macOS' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # macOS: Developer ID signing + notarization + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Update manifests are signed with minisign once the updater is enabled + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: ${{ github.ref_name }} + releaseName: WordPress Remote Editor ${{ github.ref_name }} + releaseDraft: true + prerelease: false + args: ${{ matrix.args }} + + plugin: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Package the companion plugin + run: | + cd plugin + zip -r ../remote-editor-bridge.zip remote-editor-bridge -x 'remote-editor-bridge/vendor/*' 'remote-editor-bridge/composer.*' 'remote-editor-bridge/phpcs.xml.dist' + - uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + files: remote-editor-bridge.zip diff --git a/README.md b/README.md index 8986249..b40b6dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,80 @@ # WordPress Remote Editor -A native desktop editor for WordPress sites (macOS, Windows, Linux). +A native desktop app for macOS, Windows and Linux that lets you write and publish posts and pages on any WordPress site, using the site's full block editor (Gutenberg), with every block plugin installed on that site working exactly as it does in wp-admin. + +- **Native shell**: real native menus and shortcuts, macOS vibrancy and traffic lights, Windows 11 Mica, GNOME/KDE-friendly styling, OS light/dark mode. +- **Full block compatibility**: the editor pane runs the site's own block editor, so third-party blocks, sidebar plugins and meta boxes all work. +- **Role-aware**: only offers the content types and actions your WordPress role allows (for example, contributors get *Submit for Review* instead of *Publish*). +- **Secure by design**: Application Passwords only, stored in the OS keychain; HTTPS enforced; isolated, origin-locked editor webview. See [docs/SECURITY.md](docs/SECURITY.md). +- **Installers**: `.dmg` (macOS, universal), `.deb` (Linux, plus AppImage), `.msi` (Windows). + +Read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the design and the reasoning behind it. + +## Requirements + +On the WordPress site: + +- WordPress 6.4 or newer, served over HTTPS. +- Application Passwords enabled (the default in WordPress core; some security plugins or hosts turn them off). +- Optional but recommended: the **Remote Editor Bridge** plugin from [`plugin/remote-editor-bridge`](plugin/remote-editor-bridge), which removes the second sign-in when opening the editor. + +On your computer: macOS 14+, Windows 10/11 (WebView2 is installed automatically if missing), or a Debian/Ubuntu-based Linux with WebKitGTK 4.1. + +## Development + +Prerequisites: Node 22+, Rust (stable), and the [Tauri prerequisites](https://tauri.app/start/prerequisites/) for your OS. On Debian/Ubuntu: + +```sh +sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libdbus-1-dev +``` + +```sh +npm install +npm run tauri dev # run the app with hot reload +npm test # frontend tests +cd src-tauri && cargo test # Rust tests +``` + +Live tests against a real site (for example a local one on `http://localhost:8888`): + +```sh +cd src-tauri +WPRE_TEST_SITE=https://example.com WPRE_TEST_USER=me WPRE_TEST_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \ + cargo test live_tests -- --ignored --nocapture +``` + +Companion plugin checks: + +```sh +cd plugin/remote-editor-bridge +composer install +vendor/bin/phpcs +``` + +## Building installers + +```sh +npm run tauri build -- --bundles deb,appimage # on Linux +npm run tauri build -- --target universal-apple-darwin --bundles dmg # on macOS +npm run tauri build -- --bundles msi # on Windows +``` + +Pushing a `v*` tag runs [`.github/workflows/release.yml`](.github/workflows/release.yml), which builds, signs and notarizes all three on GitHub's macOS, Ubuntu and Windows runners and attaches them to a draft release, along with a zip of the companion plugin. Signing secrets are listed in the workflow file. + +## Project layout + +| Path | What it is | +| --- | --- | +| `src/` | Platform-adaptive React shell UI | +| `src-tauri/src/` | Rust core: REST client, sign-in, keychain, editor webview, menus | +| `src-tauri/capabilities/` | Least-privilege permission grants for each webview | +| `plugin/remote-editor-bridge/` | Optional companion WordPress plugin | +| `docs/` | Architecture and security notes | + +## Name and trademark + +"WordPress" is a registered trademark of the WordPress Foundation. Its trademark policy restricts using "WordPress" in product names, so the app name should be cleared (or changed to something like "Remote Editor for WordPress") before public distribution. The companion plugin already uses a compliant name. + +## License + +GPL-2.0-or-later, the same license as WordPress. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..63d1bf9 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,118 @@ +# WordPress Remote Editor: Architecture + +## Goals + +1. Create and edit posts, pages and any other REST-enabled post type on a remote WordPress site, limited to exactly what the signed-in user's role allows. +2. Run the full Gutenberg block editor, including blocks, sidebars and meta boxes added by any plugin installed on that site. +3. Install natively on macOS (`.dmg`), Linux (`.deb`) and Windows (`.msi`) and feel like a native app on each. +4. Follow WordPress standards (REST API, Application Passwords, capabilities, coding standards, GPL) and current desktop security practice. + +## The key design decision: where Gutenberg runs + +Gutenberg is a JavaScript application, and every block plugin ships JavaScript that is written against the exact versions of `wp.blocks`, `wp.blockEditor`, `wp.data`, React and so on that the site is running. Plugins can also add PHP-rendered meta boxes (ACF, Yoast, WooCommerce) and enqueue arbitrary editor assets. + +Three ways to get Gutenberg into a desktop app were considered: + +| Approach | Plugin compatibility | Notes | +| --- | --- | --- | +| Bundle our own copy of `@wordpress/*` packages and render blocks locally | Poor | Version skew with the site; plugin block scripts can't be loaded reliably; PHP meta boxes impossible. | +| Load the site's editor scripts into a local shell page | Partial | Needs a server-side asset manifest; meta boxes and many `admin_enqueue_scripts` integrations still break. | +| **Load the site's own block editor screen in an isolated, hardened webview inside a native shell** | **Complete** | The site runs its real editor, so every block, sidebar plugin and meta box works exactly as in wp-admin. | + +We use the third approach. It is the only one that can honour "fully compatible with any kind of block plugin". Everything *around* the editor canvas is native: windows, menus, keyboard shortcuts, site and content browser, toolbar, dialogs, notifications, credential storage. The wp-admin chrome (admin bar, admin menu, the "W" back button) is hidden so the editor reads as part of the app, not as a website. + +## Framework: Tauri 2 (Rust core + system webview) + +| Option | Why not / why | +| --- | --- | +| Electron | Ships a whole Chromium (~150 MB), larger attack surface, looks the same everywhere. | +| Three native apps (SwiftUI, WinUI, GTK) | Most native, but three codebases and we still need a webview for Gutenberg. | +| Qt / Flutter | Custom-drawn widgets, not truly native; still needs a webview. | +| **Tauri 2** | Uses each OS's own webview (WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux), real native menus, dialogs, window effects (macOS vibrancy, Windows Mica), ~10 MB installers, a Rust core with a capability-based permission model, and first-party `.dmg`/`.deb`/`.msi` bundling. | + +### Native feel per platform + +The shell UI is React, but it is *platform-adaptive*, not a single web look: + +- **macOS**: overlay title bar with traffic lights, translucent sidebar (`sidebar` vibrancy), SF Pro, macOS control metrics, ⌘ shortcuts, standard app menu. +- **Windows 11**: Mica backdrop, Segoe UI Variable, Fluent spacing and focus visuals, Ctrl shortcuts, native title bar. +- **Linux (GNOME/KDE)**: native decorations, libadwaita-like header and list styling, system font (Cantarell/Noto). +- All platforms follow the OS light/dark setting and accent colour, use native file dialogs, native notifications and the OS keychain. + +## Process and trust model + +``` +┌──────────────────────── Native window ─────────────────────────┐ +│ Shell webview (trusted, bundled assets only) │ +│ React UI: sites · post lists · toolbar ── IPC ──┐ │ +│ ▼ │ +│ Rust core (all network + secrets) +│ · WordPress REST client │ +│ · OS keychain │ +│ · window/webview manager │ +│ ┌───────────────── Editor child webview (untrusted) ──────────┐ │ +│ │ https://site/wp-admin/post.php?post=… (site's real Gutenberg)│ │ +│ │ · navigation locked to the site origin │ │ +│ │ · per-site isolated cookie store │ │ +│ │ · IPC limited to one command: editor_report │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +- **Shell webview**: loads only bundled assets, strict CSP, never talks to WordPress directly and never sees a password. +- **Rust core**: the only component that holds credentials or performs authenticated HTTP. HTTPS-only (plain HTTP allowed solely for `localhost`/`.local`/`.test` development sites after explicit opt-in), rustls with the OS trust store, redirects restricted to the same origin, response size limits. +- **Editor webview**: treated as untrusted remote content. It gets a runtime Tauri capability scoped to *that site's origin and that webview label* allowing only `editor_report` (dirty/saving/status/title state for the native toolbar). Native → editor control (save, publish, preview) is done by the core evaluating a fixed script; nothing from the page is ever evaluated by the core. + +## Authentication + +WordPress **Application Passwords** (core since 5.6) are used for all REST traffic. + +1. **Discovery**: fetch the site, follow the `Link: <…>; rel="https://api.w.org/"` header to the REST index, read `authentication["application-passwords"].endpoints.authorization`. Works with pretty and plain permalinks (`?rest_route=`). +2. **Authorise**: open WordPress's own `authorize-application.php` in an isolated, incognito auth window. The user signs in with their normal login (2FA, SSO and security plugins keep working) and approves "WordPress Remote Editor". `success_url` is `wpremoteeditor://auth/callback?state=…`; the core intercepts that navigation inside its own webview (so no other app can register the scheme and steal it), checks `state`, and stores the password. +3. **Manual fallback**: paste a username and an Application Password created under Users → Profile. +4. **Storage**: the password goes straight into the OS keychain (macOS Keychain, Windows Credential Manager, Secret Service/libsecret on Linux) and is held in memory only in zeroizing buffers. +5. **Removal**: removing a site revokes its Application Password on the server (using `/application-passwords/introspect` to find its UUID) and deletes the keychain entry. + +### Signing the editor webview in + +wp-admin uses cookie authentication, which Application Passwords can't provide. Two modes: + +- **With the companion plugin** (`plugin/wp-remote-editor-bridge`, recommended): the core calls `POST /wpre/v1/handoff` authenticated by the Application Password. The plugin returns a single-use, 60-second, hashed token bound to that user; the editor webview opens `wp-login.php?action=wpre_handoff&token=…`, which sets the normal auth cookie and redirects into the editor. No second login. +- **Without the plugin**: the user signs in once inside the editor webview using the site's own login form. The cookie lives in that site's isolated store. + +## Roles and capabilities + +- `GET /wp/v2/users/me?context=edit` gives the user's roles and full capability map. +- `GET /wp/v2/types?context=edit` gives each post type's capability names (`edit_posts`, `publish_posts`, `create_posts`, …). +- The shell only offers post types the user can create, shows *Publish* vs *Submit for Review* according to `publish_posts`, and hides actions the user can't perform. WordPress still enforces everything server-side; the UI gating is for clarity, not security. + +## Repository layout + +``` +src/ React shell (platform-adaptive UI) +src-tauri/ Rust core: REST client, auth, keychain, editor webview + capabilities/ Tauri capability files (least privilege) +plugin/wp-remote-editor-bridge/ Optional companion WordPress plugin (GPL-2.0-or-later, WPCS) +.github/workflows/ CI (lint, test, phpcs) and signed release builds +docs/ Architecture and security notes +``` + +## Packaging and distribution + +| OS | Artifact | Signing | +| --- | --- | --- | +| macOS | `.dmg` (universal: Apple Silicon + Intel) | Developer ID signing + Apple notarization, hardened runtime | +| Linux | `.deb` (plus AppImage as a bonus) | Detached GPG signature; desktop entry registers the URL scheme | +| Windows | **`.msi`** (WiX) | Authenticode signing | + +`.msi` is chosen over `.exe` (NSIS) because it is what IT departments deploy with Intune/Group Policy, supports clean upgrades and repair, and is equally easy for home users. WebView2 is bootstrapped by the installer if missing. + +Builds run on GitHub Actions on real macOS, Ubuntu and Windows runners via `tauri-action`. Auto-update uses Tauri's updater with minisign-signed update manifests (added once a release channel exists). + +## Roadmap + +1. **Scaffold (this commit)**: shell, site onboarding, keychain, REST client, capability gating, embedded editor, companion plugin, packaging config, CI. +2. Media library browser with native drag-and-drop upload. +3. Offline drafts queued locally (encrypted at rest) and synced on reconnect. +4. Multi-site switching with per-site windows, notifications for scheduled posts. +5. Auto-update channel, crash reporting (opt-in). diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..ffe4405 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,54 @@ +# Security Model + +WordPress Remote Editor holds credentials for websites and renders remote content, so it is designed around least privilege and isolation. + +## Credentials + +- Only **Application Passwords** are used (never the user's main WordPress password). They can be revoked per app from the WordPress profile screen, and the app revokes its own when a site is removed. +- Passwords are stored only in the **OS keychain** (macOS Keychain, Windows Credential Manager, Secret Service/libsecret on Linux), never on disk in plain text, never in logs, never in the shell UI. +- In memory, secrets live in zeroizing buffers and are redacted from debug output. +- Non-secret site metadata (`sites.json`) is written atomically with `0600` permissions on macOS/Linux. + +## Network + +- HTTPS only. Plain HTTP is allowed solely for `localhost`, `*.local`, `*.test` and `*.localhost`, and only when the user ticks "local development site". +- TLS via rustls with the operating system's trust store. +- Authenticated requests never follow redirects to another origin, so credentials can't be forwarded to a third party. Discovery (unauthenticated) may follow HTTPS redirects, e.g. `example.com` → `www.example.com`. +- URLs supplied by the server (REST root, editor handoff) must be on the site's own origin. +- Response bodies are capped at 16 MB; timeouts are enforced. + +## Sign-in + +- Authorization happens on the site's own `authorize-application.php` page, so 2FA, SSO and security plugins work unchanged. +- The sign-in window is incognito (no persistent cookies) and has no IPC access. +- The callback (`wpremoteeditor://…`) is intercepted inside that window, not registered with the OS, so another app cannot hijack it. A 256-bit `state` value is checked in constant time. + +## Webviews + +| Webview | Loads | IPC | +| --- | --- | --- | +| Shell (`main`) | Bundled assets only, strict CSP, prototype freezing | App commands listed in `capabilities/main.json`; cannot call `editor_report` | +| Sign-in (`auth`) | The site's authorization page | None | +| Editor (`editor`) | The site's own wp-admin block editor | Only `editor_report`, granted at runtime for that site's origins; payload is strictly validated | + +- The editor webview's navigation is locked to the site's origins; other links open in the system browser. +- Each site has its own cookie/data store, deleted when the site is removed. +- The core controls the editor only by evaluating fixed, built-in scripts. It never evaluates strings that come from a page. + +## Companion plugin (`remote-editor-bridge`) + +- Handoff links can only be requested over REST with Application Password authentication by users who can edit content. +- Tokens are 48 random characters, single use, valid for 60 seconds, stored only as an HMAC-SHA256 hash, rate limited to 10/minute per user, and can only redirect to `post.php`/`post-new.php`. +- The standard `wp_login` action fires so audit/security plugins see the sign-in. +- Passes WordPress Coding Standards (`phpcs`), escapes all output, and stores nothing permanently. + +## Releases + +- macOS: Developer ID signed, hardened runtime, notarized. +- Windows: Authenticode-signed `.msi` (configure `bundle.windows.certificateThumbprint` or `signCommand`, e.g. Azure Trusted Signing). +- Linux: `.deb` with a detached signature. +- Auto-updates (when enabled) are verified with a minisign public key embedded in the app. + +## Reporting a vulnerability + +Please report security issues privately through GitHub Security Advisories on this repository rather than opening a public issue. diff --git a/index.html b/index.html new file mode 100644 index 0000000..fb0ac86 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + WordPress Remote Editor + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d4dd81 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3233 @@ +{ + "name": "wordpress-remote-editor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wordpress-remote-editor", + "version": "0.1.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-os": "^2.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.20.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^26.1.0", + "typescript": "^5.9.0", + "vite": "^7.1.0", + "vitest": "^3.2.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.5.tgz", + "integrity": "sha512-MrfgRQSpD5hkkRg6XI16B6EDLb+6+xp8xje6bpdSwkYw+qxrBKbUVrRAQu9h82UT+QumSIVQ8HXsXjPd2dlhqw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.5.tgz", + "integrity": "sha512-+uu2p/R9Tzn8GpoQyX1pAo5dmKT1DhXevfaCzqzmPFhnLjHLWw7uWkaVGlxmhwYoRiYYhsGfRcD5cTBpL7rgSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.5.tgz", + "integrity": "sha512-17dys+YEmzwzzFVHQAVDONg5FALXY9kd99HFEpfk4gJVHYxoh5bGea3D4t1fXXnwkTcOe25wPyd6nWS0I8IdLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.5.tgz", + "integrity": "sha512-wg0zQEKkEvFZf1UaaX1UELOoPs//OSKUG7D+yITuqOsAvza7LvHTv5Wu4nmMfzNG7EYI2jp2PJC7Id6OETmsYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.5.tgz", + "integrity": "sha512-CoJmWcpEMuwin+TZ/ev/H9uDyF/uxUKQSWXdmIPn/UBx/uGV9+4ebMp0FwnlhWR9PE1AdzYsKf3bZevhif38+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.5.tgz", + "integrity": "sha512-PxZnLAaTUAL5xG0W2BXLeYPKrK2b3LIASV2/e/Kf7PZvSLI1iAQIm9B9aJD+YyFIT7hc6qaMV3+OOO35R28LfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.5.tgz", + "integrity": "sha512-uPo3IvnONjhbFXwWgroBeFwOicuhT7ZwQrnzkzmOtBJzeQbkUJUE1SJDfDI31ijQ5+ZUIA6sPrdUMxTSnxUWAg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.5.tgz", + "integrity": "sha512-ywI8Jyuc4LyHYTjC0mw4yA1ulvZBkHusdpWnoiEBsmBuHvSnt90btTBGCZ+s/2y+YNNfVAEKy33nPo5UI9zTmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.5.tgz", + "integrity": "sha512-tmuXJoQ/3ovSr637aykZMdQteU/CB2APAz2CrvsbibuoHZi1BS1Db13cVViBdEARGwXCLn1DSJ8wsjoThLJPGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.5.tgz", + "integrity": "sha512-5FMjSYNcUOHx7oW8Qu9KB6jGPpvF/8sG7+BDHwU/Ihh1/6FMJ9ySmsFG4tJ1qbNRuP8OLzCBDso4sF0mQaLuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.5.tgz", + "integrity": "sha512-K8ZJ+neLDbUkIrL8dqF/5LtuVCrKwo27kREgOlxjBEjlOXwHvDnHLU8Z/UKv/B49qi7e8hsSDOyoJwhwqhvX4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.5.tgz", + "integrity": "sha512-NlVxsq7PFPv0DBKGuNWRDKixQYQCjM53zl04mxEcKMBmJbCBIUm1JdPfIWDRHyOOFzzYPmLQzfeEwh4716ayQg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.5.tgz", + "integrity": "sha512-ARhRHJYs5eCFet5azfrHD97vOkD8OzNW2RSMlvo5CcBqXaTOLfp0OHDtUQh7/K5AL5fUYoFp2fgC9KDg4vVYMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.5.tgz", + "integrity": "sha512-atSgreM5C7jpxd9aEXN+6vJ5wTd05iYw+S3FiQpw7VsoEl8B90llbq35ARON+GOHj5QlVQEq2vOKWCHTob/BSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.5.tgz", + "integrity": "sha512-vmlmwBFGnjVWxSRoVabNkyYuscSmg52Pd0fM8jEbNT3Jmka4A6HcB84x4uAmuumznWfBVaxKOqU6jbEgomWKCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.5.tgz", + "integrity": "sha512-hhJrP6S9WkbsbcHgUkvCBS5Cn8IdsG8XjlLZyxjnv9+NdoYfbbZE4TWJYr8apdaj1M5t5Y1id2StnEslxpNFyA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.5.tgz", + "integrity": "sha512-NNVCyN9pGrwFmQa4bf+Cnf5aVH3g6ZpnlOg1Cs/H9swIHT9bRixijfzUqyEIWm1WtiAuR1LJ+rJH6zpBm8kgQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.5.tgz", + "integrity": "sha512-dR9nd6OKnvSS60Ifz/L7Mj5zzYyoqyRn2NwTuaOONKaa3xsVGXyp8YusMbvFyEOR94DxnwgcbgsFqkHd29ZSzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.5.tgz", + "integrity": "sha512-1tzsMjaqhsh3bAO6SJiJqh8hIr9sSPRd+i2vPPxJHod8/XYnf9TEnK0z/u3r1z1AkI347X0KbRSg9dWcxeGahw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.5.tgz", + "integrity": "sha512-01mWMQblKoXXsmk28KXtpbA96w7g/aOWUoV3AC16fdHdqkykGalqyyM+5u8M7uP5/MEMiiPv69Hv7iv/hEH/dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.5.tgz", + "integrity": "sha512-trdnMPb/DNm4V/CgsaoGgr90HfQQ071wfK0KoIs4YpLvjBgVQGXhGabg2Z6nK4EcYNilrb9ZZPcsP6+zpJIdxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.5.tgz", + "integrity": "sha512-x2LxZlfdfsw99T6/qb9EqE/Qi84JWzibjyOJyhJsgFJy6CafaTiqIXCCnslCGrV5XWNh7fwrS3FbRENcuKnooA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.5.tgz", + "integrity": "sha512-/Qd/9aDQIo0+jmItgBmPIs8195YVbDxRRApn47GyufgbutCDOh7eP5vSKKlI4QNFeaI+wKQAkSMsPnF3EuMpaA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.5.tgz", + "integrity": "sha512-ngUJ/XvOWrqbd2IMsfXfh2xXFLTP3sY7sCKvxbGvYdBZ1KmXFd3ewDZ9n3/GNzAUgzRuhLT/UvJRivbNoAQRBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.5.tgz", + "integrity": "sha512-IhV6ew/DuzUWwHaYmlh7MrPCV3lKFUq/amTEwysi4r/mwNPAEfAMh5dYWeTSPTeMy2pFHGMEWYtPkHzfX3tG5A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.3.tgz", + "integrity": "sha512-9s0PKw0p4r3kFq92jhpivYSp+cSht15o0gXpZxuQd0+dV9KCexEVkNaLELVoLdJWyoH4KlfQo5h3W2LIM8siJg==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.2.tgz", + "integrity": "sha512-PMSppzAnlT+BvZtRKuidxjRSm2r5t8kDpJ/czmmQUa0AtDNTwrjLE8vApK3phCWGg7isfnYL46Dl1jB4ArI0hA==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.12.2", + "@tauri-apps/cli-darwin-x64": "2.12.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.12.2", + "@tauri-apps/cli-linux-arm64-musl": "2.12.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-gnu": "2.12.2", + "@tauri-apps/cli-linux-x64-musl": "2.12.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.12.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.12.2", + "@tauri-apps/cli-win32-x64-msvc": "2.12.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.2.tgz", + "integrity": "sha512-Lk3zaBTfzNvA7670Al5PNgo2M/95uaDPe1yp+ILJeDOmol87XiYuK5E5+FUbK5e6U6OORx9qpZdOo2U+5tETtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.2.tgz", + "integrity": "sha512-2CyYjJ833JB3sKRz4B0W/Tv4oOVC16ws1bxdSCWj1XvrmanWwvZi99e5tNIoCHwHhcOm0jh+sh0PCGQyls6Z+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.2.tgz", + "integrity": "sha512-uvt2xbone4JftKarelssi86Kqnl7JiG1Smh5+Ae+nZmtM+JUhTGmqkcqfUPXRW8Ygwog92POZvd4oGDN9j8eXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.2.tgz", + "integrity": "sha512-DZlDJ24u3tlocKOYqzR+b2M+Qr+5WFcoMBZ7EfdhHN5j3ZXwBBpms4G4gyNvPCrak5vMzmdZ65ecBXPJcC+5sA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.2.tgz", + "integrity": "sha512-pb+RTZUYH/p0gR4j4ecCCq+FNfz3W3eKuUQvVEBn7MQZx6DzfLO7FaYzt2NyDKWpdNN3lP10mdzOvw3R5P5aAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.2.tgz", + "integrity": "sha512-nidEvDUJyHYNbIv9MUk6KiEix8IXfRh4Wj2G2SA5g8nFmgbBCwazUNWYiG/LhNeNbfZpBMLerRBUtoQ5K0GidA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.2.tgz", + "integrity": "sha512-gkXyYn3w+Xfc8nz/DleEbkm1M7bBuU2cejqUXNmI98cA2P5kp+oPmS4ALTorb1FaNWjpsKoYZBWsbS9BXxYFkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.2.tgz", + "integrity": "sha512-MrjgoZ09s8Hsn99XvxSNvK8NQTZxi7XiyK3FxxytzfHgz2jb3qUT69CZtsokRkN/zdi9rCgrMtq+w9XNICjcfQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.2.tgz", + "integrity": "sha512-BR+CYM7iF3lkfyTtVkwMQU/s9hwegLpj3UqDjaxKCPmixQCa1csBBPy3C75V+FhLtOjIXNna4Gt6Vm9oxbV+qA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.2.tgz", + "integrity": "sha512-zODJo9l3TbNBM+GrO/yPqGeELNMK5cjIc+QHh1WQc3l8tD+jDZGTQyYuAxViCsM6Uh5uWJ//RnCZG9LXrvsXww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.2.tgz", + "integrity": "sha512-cQXI2ZaZmwBDt0XATCqASVanYizxkEE3ceQVlVHXaQ51HzsQUWA04c9Nz7LrrY2lgAu3jFMyat+8Yz3Z2R2qBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.2.tgz", + "integrity": "sha512-LFqp8OTx0bHOwhKRhMMDyRDlo/2IO5XxoOclpf8+yvjAfM2ytVdbB6tk63dBlicsR8pRw38maj/GxrU6cW8mXw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.1.tgz", + "integrity": "sha512-XkXdt30rontti9TII/gBBxFRXWfj8DiprFW7RgBFRBj1OhdfjBNVcFqSEWkkZ4hXafHnuEnB7F1sFsmu6uyywQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.4.0.tgz", + "integrity": "sha512-XFQjntNNI3Vlqdy4WUGwiXX8DByzKVvoemAcUVkBm9NJPGmoDtII5i+vfuAzWaJfOE5cUJJLUDqXBnd4reSAIA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz", + "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001815", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", + "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.453", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.453.tgz", + "integrity": "sha512-9r/nz9akllr3SZsHUhfK10ktUH6j0Dbo3cKdh91khw0FvR6HhXGnATmOq5X8ena2aq4GXLQ1koh3nvKaQJaNmQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", + "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.64.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.5.tgz", + "integrity": "sha512-EAP5ZHcz2ijbgCWwPkpI7zh6bfsri2Ez5FtCy2dCp1k62duXzfG7DLGGh7RU2QQXWwavT+u4qpvDF/TWOiiGTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.5", + "@rollup/rollup-android-arm64": "4.64.5", + "@rollup/rollup-darwin-arm64": "4.64.5", + "@rollup/rollup-darwin-x64": "4.64.5", + "@rollup/rollup-freebsd-arm64": "4.64.5", + "@rollup/rollup-freebsd-x64": "4.64.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.5", + "@rollup/rollup-linux-arm-musleabihf": "4.64.5", + "@rollup/rollup-linux-arm64-gnu": "4.64.5", + "@rollup/rollup-linux-arm64-musl": "4.64.5", + "@rollup/rollup-linux-loong64-gnu": "4.64.5", + "@rollup/rollup-linux-loong64-musl": "4.64.5", + "@rollup/rollup-linux-ppc64-gnu": "4.64.5", + "@rollup/rollup-linux-ppc64-musl": "4.64.5", + "@rollup/rollup-linux-riscv64-gnu": "4.64.5", + "@rollup/rollup-linux-riscv64-musl": "4.64.5", + "@rollup/rollup-linux-s390x-gnu": "4.64.5", + "@rollup/rollup-linux-x64-gnu": "4.64.5", + "@rollup/rollup-linux-x64-musl": "4.64.5", + "@rollup/rollup-openbsd-x64": "4.64.5", + "@rollup/rollup-openharmony-arm64": "4.64.5", + "@rollup/rollup-win32-arm64-msvc": "4.64.5", + "@rollup/rollup-win32-ia32-msvc": "4.64.5", + "@rollup/rollup-win32-x64-gnu": "4.64.5", + "@rollup/rollup-win32-x64-msvc": "4.64.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..721b90e --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "wordpress-remote-editor", + "private": true, + "version": "0.1.0", + "type": "module", + "license": "GPL-2.0-or-later", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "vitest run", + "lint": "tsc --noEmit", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.8.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-os": "^2.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.20.5", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^5.0.0", + "jsdom": "^26.1.0", + "typescript": "^5.9.0", + "vite": "^7.1.0", + "vitest": "^3.2.0" + } +} diff --git a/plugin/remote-editor-bridge/assets/embedded.css b/plugin/remote-editor-bridge/assets/embedded.css new file mode 100644 index 0000000..2ec47bf --- /dev/null +++ b/plugin/remote-editor-bridge/assets/embedded.css @@ -0,0 +1,21 @@ +/* Hide wp-admin chrome around the block editor inside the desktop app. */ +html.wp-toolbar:has(body.wpre-embedded) { + padding-top: 0; +} + +body.wpre-embedded #wpadminbar, +body.wpre-embedded #adminmenumain, +body.wpre-embedded #wpfooter, +body.wpre-embedded .edit-post-fullscreen-mode-close, +body.wpre-embedded .editor-header__back-button { + display: none !important; +} + +body.wpre-embedded #wpcontent { + margin-left: 0; +} + +body.wpre-embedded .interface-interface-skeleton { + top: 0; + left: 0; +} diff --git a/plugin/remote-editor-bridge/composer.json b/plugin/remote-editor-bridge/composer.json new file mode 100644 index 0000000..645328b --- /dev/null +++ b/plugin/remote-editor-bridge/composer.json @@ -0,0 +1,21 @@ +{ + "name": "realact/remote-editor-bridge", + "description": "Companion plugin for the WordPress Remote Editor desktop app.", + "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=7.4" + }, + "require-dev": { + "wp-coding-standards/wpcs": "^3.1", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "scripts": { + "lint": "phpcs" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/plugin/remote-editor-bridge/composer.lock b/plugin/remote-editor-bridge/composer.lock new file mode 100644 index 0000000..9682f87 --- /dev/null +++ b/plugin/remote-editor-bridge/composer.lock @@ -0,0 +1,437 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "1baf6cf078b38181c98f694d660844a7", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/39467533fdb742446d68c1d10ac33d625ee0311c", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T11:13:17+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/5f35d9408c54d7b529501f3c688b6eae562aea1f", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T10:28:41+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.6", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-08-06T00:17:32+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/ec2ff942335f33683a5957a85d138753876a05cf", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.1", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2026-07-27T11:53:23+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/plugin/remote-editor-bridge/includes/class-embedded-mode.php b/plugin/remote-editor-bridge/includes/class-embedded-mode.php new file mode 100644 index 0000000..4868d2d --- /dev/null +++ b/plugin/remote-editor-bridge/includes/class-embedded-mode.php @@ -0,0 +1,65 @@ +is_block_editor() ) { + return; + } + wp_enqueue_style( + 'remote-editor-bridge-embedded', + plugins_url( 'assets/embedded.css', __DIR__ ), + array(), + VERSION + ); + } +} diff --git a/plugin/remote-editor-bridge/includes/class-handoff.php b/plugin/remote-editor-bridge/includes/class-handoff.php new file mode 100644 index 0000000..8e6f54a --- /dev/null +++ b/plugin/remote-editor-bridge/includes/class-handoff.php @@ -0,0 +1,231 @@ + \WP_REST_Server::CREATABLE, + 'callback' => array( self::class, 'issue' ), + 'permission_callback' => array( self::class, 'can_issue' ), + 'args' => array( + 'redirect_to' => array( + 'type' => 'string', + 'required' => true, + 'format' => 'uri', + 'sanitize_callback' => 'esc_url_raw', + ), + ), + ) + ); + + register_rest_route( + REST_NAMESPACE, + '/info', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return rest_ensure_response( array( 'version' => VERSION ) ); + }, + 'permission_callback' => 'is_user_logged_in', + ) + ); + } + + /** + * Only requests authenticated with an Application Password, by users who + * can edit content, may mint handoff links. + * + * @return true|\WP_Error + */ + public static function can_issue() { + if ( ! is_user_logged_in() || ! rest_get_authenticated_app_password() ) { + return new \WP_Error( + 'wpre_app_password_required', + __( 'Sign-in handoff requires Application Password authentication.', 'remote-editor-bridge' ), + array( 'status' => 401 ) + ); + } + if ( ! current_user_can( 'edit_posts' ) && ! current_user_can( 'edit_pages' ) ) { + return new \WP_Error( + 'wpre_forbidden', + __( 'Your role cannot edit content on this site.', 'remote-editor-bridge' ), + array( 'status' => 403 ) + ); + } + return true; + } + + /** + * Restricts the post-login destination to the block editor screens. + * + * @param string $url Requested destination. + * @return string|null Safe destination, or null when not allowed. + */ + public static function safe_editor_url( $url ) { + $url = wp_validate_redirect( $url, '' ); + if ( '' === $url ) { + return null; + } + $admin = admin_url(); + if ( 0 !== strpos( $url, $admin . 'post.php' ) && 0 !== strpos( $url, $admin . 'post-new.php' ) ) { + return null; + } + return $url; + } + + /** + * Hashes a token for storage so a leaked cache entry can't be replayed. + * + * @param string $token Raw token. + * @return string + */ + private static function hash( $token ) { + return hash_hmac( 'sha256', $token, wp_salt( 'auth' ) ); + } + + /** + * REST callback: create a handoff link. + * + * @param \WP_REST_Request $request Request. + * @return \WP_REST_Response|\WP_Error + */ + public static function issue( \WP_REST_Request $request ) { + $user_id = get_current_user_id(); + $redirect = self::safe_editor_url( (string) $request->get_param( 'redirect_to' ) ); + if ( null === $redirect ) { + return new \WP_Error( + 'wpre_bad_redirect', + __( 'The editor address is not allowed.', 'remote-editor-bridge' ), + array( 'status' => 400 ) + ); + } + + $rate_key = self::RATE_TRANSIENT . $user_id; + $count = (int) get_transient( $rate_key ); + if ( $count >= self::MAX_PER_MINUTE ) { + return new \WP_Error( + 'wpre_rate_limited', + __( 'Too many sign-in requests. Please wait a minute.', 'remote-editor-bridge' ), + array( 'status' => 429 ) + ); + } + set_transient( $rate_key, $count + 1, MINUTE_IN_SECONDS ); + + $token = wp_generate_password( 48, false, false ); + set_transient( + self::TRANSIENT . self::hash( $token ), + array( + 'user_id' => $user_id, + 'redirect' => $redirect, + 'expires' => time() + self::TTL, + ), + self::TTL + ); + + $url = add_query_arg( + array( + 'action' => 'wpre_handoff', + 'token' => rawurlencode( $token ), + ), + wp_login_url() + ); + + $response = rest_ensure_response( array( 'url' => $url ) ); + $response->header( 'Cache-Control', 'no-store' ); + return $response; + } + + /** + * Runs on wp-login.php?action=wpre_handoff: verifies the token, signs the + * user in and redirects to the editor. + */ + public static function consume() { + nocache_headers(); + header( 'Referrer-Policy: no-referrer' ); + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- The single-use token is the CSRF protection. + $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; + if ( ! preg_match( '/^[A-Za-z0-9]{48}$/', $token ) ) { + self::fail(); + } + + $key = self::TRANSIENT . self::hash( $token ); + $data = get_transient( $key ); + delete_transient( $key ); // Single use, even on failure below. + + if ( ! is_array( $data ) || empty( $data['user_id'] ) || (int) $data['expires'] < time() ) { + self::fail(); + } + + $user = get_user_by( 'id', (int) $data['user_id'] ); + if ( ! $user || ( ! user_can( $user, 'edit_posts' ) && ! user_can( $user, 'edit_pages' ) ) ) { + self::fail(); + } + + $redirect = self::safe_editor_url( $data['redirect'] ); + if ( null === $redirect ) { + $redirect = admin_url( 'post-new.php' ); + } + + wp_set_current_user( $user->ID ); + wp_set_auth_cookie( $user->ID, false, is_ssl() ); + /** This action is documented in wp-includes/user.php */ + do_action( 'wp_login', $user->user_login, $user ); + + setcookie( + self::EMBED_COOKIE, + '1', + array( + 'expires' => 0, + 'path' => ADMIN_COOKIE_PATH, + 'domain' => COOKIE_DOMAIN, + 'secure' => is_ssl(), + 'httponly' => true, + 'samesite' => 'Lax', + ) + ); + + wp_safe_redirect( $redirect ); + exit; + } + + /** + * Ends the request with a generic error. + */ + private static function fail() { + wp_die( + esc_html__( 'This sign-in link has expired or was already used. Return to WordPress Remote Editor and try again.', 'remote-editor-bridge' ), + esc_html__( 'Sign-in link expired', 'remote-editor-bridge' ), + array( 'response' => 403 ) + ); + } +} diff --git a/plugin/remote-editor-bridge/phpcs.xml.dist b/plugin/remote-editor-bridge/phpcs.xml.dist new file mode 100644 index 0000000..80b9c91 --- /dev/null +++ b/plugin/remote-editor-bridge/phpcs.xml.dist @@ -0,0 +1,24 @@ + + + WordPress Coding Standards for the companion plugin. + . + /vendor/* + + + + + + + + + + + + + + + + + remote-editor-bridge.php + + diff --git a/plugin/remote-editor-bridge/readme.txt b/plugin/remote-editor-bridge/readme.txt new file mode 100644 index 0000000..1bc6c18 --- /dev/null +++ b/plugin/remote-editor-bridge/readme.txt @@ -0,0 +1,34 @@ +=== Remote Editor Bridge === +Contributors: realact +Tags: block editor, gutenberg, desktop, application passwords, remote editing +Requires at least: 6.4 +Tested up to: 6.8 +Requires PHP: 7.4 +Stable tag: 0.1.0 +License: GPL-2.0-or-later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Companion plugin for the WordPress Remote Editor desktop app. + +== Description == + +WordPress Remote Editor is a desktop app for macOS, Windows and Linux that edits your site with its own block editor, so every block plugin you use keeps working. + +This optional plugin improves that experience: + +* **No second sign-in.** The app signs in to the REST API with an Application Password. This plugin lets it exchange that for a single-use, 60-second sign-in link when opening the editor. +* **Native look.** The wp-admin bar and menu are hidden inside the app so the editor blends in. + +The plugin adds no settings, stores nothing permanently, and changes nothing for normal wp-admin visits. + += Security = + +* Sign-in links can only be requested with Application Password authentication, by users who can edit posts or pages. +* Each link works once, expires after 60 seconds, is stored only as an HMAC hash, and can only redirect to the block editor screens. +* Requests are rate limited per user. +* The standard `wp_login` action fires, so audit and security plugins see the sign-in. + +== Changelog == + += 0.1.0 = +* First release. diff --git a/plugin/remote-editor-bridge/remote-editor-bridge.php b/plugin/remote-editor-bridge/remote-editor-bridge.php new file mode 100644 index 0000000..2c5b9f7 --- /dev/null +++ b/plugin/remote-editor-bridge/remote-editor-bridge.php @@ -0,0 +1,29 @@ +query( + $wpdb->prepare( + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", + $wpdb->esc_like( '_transient_wpre_handoff_' ) . '%', + $wpdb->esc_like( '_transient_timeout_wpre_handoff_' ) . '%' + ) +); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..2bcfe3d --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5954 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.8+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_info" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b" +dependencies = [ + "android_system_properties", + "log", + "nix 0.31.3", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.8", + "serde", + "sha2", + "zbus 4.4.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.3", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c404ecf956ff241bf6b39dd688614724e1bf1f63255abb4fc46d1c40371672ea" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.5", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eea0b6302f82c574b21af6fc6ae6e86183b9280853c22621d0abf0936911ffc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f468f9a9bcd404f57e5c191d91024e3de953d4b610f78c2907e7110615cb1c" +dependencies = [ + "base64 0.23.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131bffec939b20642380317f8403a33c8888489925e14bb965ceaff0d949b1cb" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1140cf34a3b3b836a13103dcab17f18831d5cc3534cbd435dc01a5c6daa65aa2" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dca325d6e66e9170f2e1b9631722e4e544bcbc51781575d266abdaba697bd51" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213fae2d85d4b76b06de5915d1862db9fa74541171f9d2c15da6ac9bd6e788c6" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85dead674ee41937d042ee855d785f07ee67206f19402200c12b9255964d5ac8" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "url", + "windows", + "zbus 5.19.0", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd69d272ab772b30462779de12a40f837616831c794c71531bea91474e1061f" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c142ed88deee222bd2d979269d35c73b6c1c0f6ebd5b79b4ff80066fcad6af1" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.21", + "tokio", + "tracing", + "windows-sys 0.61.2", + "zbus 5.19.0", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wordpress-remote-editor" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "keyring", + "log", + "rand 0.8.8", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-os", + "tauri-plugin-single-instance", + "thiserror 2.0.21", + "tokio", + "url", + "uuid", + "zeroize", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.8", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros 5.19.0", + "zbus_names 4.3.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "zbus_names 4.3.4", + "zvariant 5.15.0", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive 5.15.0", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.9", + "zvariant_utils 4.2.0", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.9", + "winnow 1.0.4", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..f35c119 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "wordpress-remote-editor" +version = "0.1.0" +description = "A native desktop editor for WordPress sites" +authors = ["WordPress Remote Editor contributors"] +license = "GPL-2.0-or-later" +edition = "2021" +rust-version = "1.80" + +[lib] +name = "wpre_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["unstable", "macos-private-api"] } +tauri-plugin-opener = "2" +tauri-plugin-os = "2" +tauri-plugin-dialog = "2" +tauri-plugin-single-instance = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls-native-roots", "http2", "charset"] } +url = { version = "2", features = ["serde"] } +uuid = { version = "1", features = ["v4", "serde"] } +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] } +zeroize = { version = "1", features = ["derive"] } +thiserror = "2" +tokio = { version = "1", features = ["sync", "macros", "rt-multi-thread", "time"] } +base64 = "0.22" +rand = "0.8" +log = "0.4" +chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +strip = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..8931257 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,23 @@ +fn main() { + // Every app command must be declared here. Tauri then generates an + // `allow-` permission for each one, and a webview can only call + // commands that a capability file explicitly grants it. + tauri_build::try_build(tauri_build::Attributes::new().app_manifest( + tauri_build::AppManifest::new().commands(&[ + "discover_site", + "begin_authorization", + "add_site_manual", + "list_sites", + "remove_site", + "get_site_context", + "list_posts", + "trash_post", + "open_editor", + "close_editor", + "editor_set_bounds", + "editor_command", + "editor_report", + ]), + )) + .expect("failed to run tauri-build"); +} diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 0000000..ec69700 --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1,31 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "The bundled shell UI webview only (not the editor child webview in the same window). It never receives credentials and cannot call editor_report.", + "webviews": ["main"], + "local": true, + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "os:default", + "dialog:allow-ask", + "dialog:allow-message", + "dialog:allow-confirm", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "https://*" }, { "url": "http://localhost*" }] + }, + "allow-discover-site", + "allow-begin-authorization", + "allow-add-site-manual", + "allow-list-sites", + "allow-remove-site", + "allow-get-site-context", + "allow-list-posts", + "allow-trash-post", + "allow-open-editor", + "allow-close-editor", + "allow-editor-set-bounds", + "allow-editor-command" + ] +} diff --git a/src-tauri/entitlements.plist b/src-tauri/entitlements.plist new file mode 100644 index 0000000..050df07 --- /dev/null +++ b/src-tauri/entitlements.plist @@ -0,0 +1,11 @@ + + + + + + com.apple.security.network.client + + com.apple.security.cs.allow-jit + + + diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..9dab72f524a73dbe3d3a5f8756ae247699e29a5b GIT binary patch literal 5894 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9a4d46ca4f6b3233008daba39ef7bbd140655fde GIT binary patch literal 10551 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..25b03d6c50537fb1f8ae394866f607d71e3ad938 GIT binary patch literal 1753 zcmV;~1}6E5P)W^QcMNXlzZjqF}Kth-hg|MW|N$%N8GfASn35qJpnqhWrzuf*=YO zTBy>Z1w|<+Xiaah6)IKqN42eKO{#t6+4LqicXwuvXJ&VkyLs702ZrosXU_TNJKs5T zu7qnEhQXW$$A4Wh(}zu)P}n(PXf89dvvzHVoXOABleM*RXm7(b`u~Fgve8EV+PRq# zKwF#P?eT)vXD@F?hFj=yDF6z_7}B^VAQNkurl+&S^S+J`7w((vBMrcvyxpwbem2Xz z?B`ylem$ZBqeJ*Fg9#5f_aG?tedm|h#&14eGy()7VS=S;=D6mWzrXTxjq8Kr<$ykO zK;(9n;MeC^Tl47XpeHnbMWY{%_IBj=G_9Kw0xOgQ2bf;-%wL;jrtXt)Y>=FTAJIwB z$izqy!QgbFNe_(oR8=rKIlYgo00S>O-&dvn?VWp?_gZ1*^Q|hSPNU5htpajrIEy`L zO-MWjPd%Jd^J@j#_Zd-;GeLPXIIP*h&QVzk(1CED5QSDHvW-vE>)yu3UI)zqu%~wb zj9$tJ01^Ou47<57YT$(@YQyIquMMsz1oLY+zVp$LS5w6ugI1Ig0Mil+QQNS~_&jJ| z&~yu0U;zLU0QZ5OZ(1M{BE>cO#~T+&WBIrHjfv=ScC`@)FZ^Wfm~jS@nq;7hGSBWa z4wua&q`;SgNU-niIWzoq{?gdIO3CT}Q>q-Jzzq#6^Xl_0IlrR1Mk(zy>{il5(j&pZ zs*@|n_m%;$=(?C)L~)`B&{6_n8A=DGb#7U`Sw^U>W-nzJ1S?!+c9ws*F z2&f?n1p%Sd*O)4VFewY-le;i5cXAiEnC+pfAztbmGoBQ5sELzUwb+Q{DkQcvgNoA6 zsz||TkOM_NbFxSveaisM!hnvP#|&5>#v~O5U{OPbq4*=`Y?|^LEj!UQ=-s}gN@t|O z{*g*}!)e63}NDtb8~b@hO3J(P_oWng5P?Zpf+28P;uZQVRwd> z49$cFjKQ*Z9A{{%bLE_Cq8)zNk%7?*3>t1Ro!(pF&*v6GZKeQ$VzI6gTlwh#aLWLi zG7y-6n5nfSQ5jAoUhNX%rx!C&yvR)MW`uh`D2ck;DC>!K!^g!PWthPk7_eoMIw+Gq zz1<|^D@akgDXJ>T3=}#8ufMs|`{uh}7|dIY!9DMn02Uh0T&1Rjk5|?*o2*c%X5|1d zX&`B6OVM~qj{o+YA9EYB8G(}Sp=A2LL~6C*WcT0fRP8ne>voOh>YJU)dMM_=T1!4b}7roJ!G&-3YiKWa*st~q>2diCp6FkqWf5p~%%1zEZki|byJAuLrMBj%Fiv8R`Q zy|2Q7BVC;jtS-Amu}p;R&O&9Tv@tC8cW_zbGhRW*qhN#N$_TD>*^J+{k#0`@X7|{= zjAwkt zpFQ&6?#Vu;|7qsXwU<>K!&j}^C90~H;q^47g~BOzyrs@qanEdfwl?4XptP-X%D^0k vJuwLm@~s-uD*hJ$sWoPf-Z@O?w0!&nZ1FrrUzFpq00000NkvXXu0mjf5vyBq literal 0 HcmV?d00001 diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..f13e109fe08d5f5dca1bf7aff39f0907739053c5 GIT binary patch literal 3170 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 IM6N<$f(H}mqW}N^ literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..17cc33e1dd3e536e68d36afe58bed410fe49f9e7 GIT binary patch literal 5043 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..398cd21cc70463817f303a0ea361d793a00bea41 GIT binary patch literal 11361 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 Js1`F0`ag-O5d{DM literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ed3cf4a809762c3a9031c3da1b10bc88767e5f8f GIT binary patch literal 1608 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2cab6f8a6a136af71676af3716c7d4aa83fb1225 GIT binary patch literal 2272 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 JPDHLkV1m)e^K}3K literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..83e6ad2a1c61084356a38f0bc5431b5c585f1a02 GIT binary patch literal 4384 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

d`8CVXwKHU@D$!jK@FV~ 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b1cb8718570ff42f7fd1cd23396ee5056724fa20 GIT binary patch literal 21160 zcmb@s2UJsC*DksfdT%OCY0{)f4!cf=Cerkx--v2qd9{fHdjN2B?TM=_T~u zq!%Id-a|_h#yL0HS=l>#wK>;Zb3V_^1^^I14lptT#1{|H0|o#) z;u#y;Ki8yW03c05yu}lBI619D3$XpZT}VW^va$i5X+p(*HnnD_ws*&R5-+!n-)Zib7@JV zQz^+dma2A6rgv)R{CNcsCh$Tfi^A+|+~MK6dHzSg;;`ea!vH6MUn**uJF9kdd;q%O zamBl$0AT~7n((bHn5msA$}zlGW@&k|}B;FVUsQv;d+m?!UxK8w>!1s23F zXnK;X13YJH+ceQ8ve9yv1NDKWlx9;r5&(ZFiyIZ3l#NUMVtqM1DL=Qj3fCp!+w8iI zQC3~ZQMqb?&CT;ZF8W+T{sF9Kd_PySyiFEj=f9Ht=Acu`wtAxHX~1^>jQf#2T(!j) z-?U;^5b~}J1m-DK%!?Qm%%E#{!Z|YaMCMqk7+>aPI?#{HJoeX~Lfw48tvhNAW(1tl zKIva|?!DVhg}4vw7M=~pj0suXh^yVKD43MxE4=wmJ3SZp3OJ>3$!khyH*(`!L;NaY z@HLZOV5{T7pz(2RLum3ESKD; znGmFf_Rugj%5K9@L$IAP6wCd<3&9{RoCp%DNo`S^uahMZ09a*QzIN6u10#6C*n#_C z#04POC+-WtgNt$Kc+FS9WZgluQb7BTfyU2UR)PN?wtT)@*#6q`tk+Ep0FZY7AKP-_ z{vQ?5g)$xhvxSoj)1A zzk-H{Q+#|J^X<@zU$S2tLFX^A{B&GFH}$xnAUETnNEK7h>9SBNz3!-vqqmiCS%am4_R^-6A=5VK7`osirJO!kRjDUDXlK#qTX*r%702K=mAE_>>e z>oJ*XtD9k$794!t{BkZ5S2I0wWEY{AYTqRrd>9_)+@BFq8pr-sE{(HaV=48|{RxiL zXuXDy4sI%o>ZJu0KBYrKC)c4Q;JnnskaiW^qO0qBuCT@e?T?{XU&)*|k>*T|km)2h zuz4b&lb>V62NeTC#3GuPQif8@qObUGmZn`gupW%I|Fo~?tX-~Y7w;e)M2Ve!A;x;W zz-E*Tl3Wm$`z=*zyZQn5QBiukqrAj^najxp?r5v9ftAonQTj3NylpDo^Z0_fV7(U( z;&vlKUT?;JjzgH{=PEXT)lc3N@nyZf&S|lO_B@j_ocpt2NueB{^3RBN&8fB|@{E#t zopWyY$D)QbnD#Z^r@DJydyPK|T2>Ou>R2J{hoQycdu`@5rM4XL=goTQ%J^kEWg%H+ zZoS{4UlTuV?wyA@vkWR$eRHTa8p3KbHc8dj-@wWIOW74Oq8@hjCS2{V%5>_L$XNrj zWe)^j7Em#oQxotMJqxot9&?S^^7vI2)WFmdUqLEVyLwNLwBJ^~M8G*3N>_Bzd^tEt z`=0eNC4O-V1-H14%h}ICz6ifte^$v}^DDTAzjQ81nWa(YxuY#ankDWsSF~9n-Rw^q zFFCzgrR*cty#i#Ji^W3v=-!74uF+H7E$&>`cw^&o{!)-D${$(KCG)&oNb~KeS!m1e zdkZA0hp$SLefPF53tr{XnH#+0vEPamRNV^m`gEd=MxKNe?b zU))v1?v9--q;JlByLX~8^(H+0@z1o;4UKte&X?l0f~j|Mi`^ z?fzYQRWnSn7jz!Bv->%O+qeB_z~r>-iSz;q6<4%?1wT~_i#rE$t6bIpa~}F%b%FVS znYp_(sz>jl;W1vA-%T-fHbe)4#{BRu7<;pYxq@)Cn5q@tK6R?Qw5x$veQ4Ua;+MqRQ~CuXPlXn{rb4s#kKi6#dbK=0=q6(OY#UPoVLoHd3zjHz*cIUy`N^$folKlx`}P}-g&;BzuPv_ z3R(s6cLX=*Xa{-E(P|ZyF)~@9^|-3B-(Y6h@Iutt&vfK*nZ-uM`VOvdLobn{BWY*m z+&xBgn+4Ug?3@U44t-l_Edb49W!{5XkJDgyubA~Wl1%1yHNPD6b5V4d8ONWBjtJ^q z8`WI$*a@y&hlbPu4bsb(1-S8j?d=TSrwb$m>b!R$$oKZR{ckf zpVYsD!L-*_WOVH2^~87^QvY~3Nts-*kgeaFOdD57&;u6q{*>Tw({NJ+=~rdlSmhD7 zXF@#CIoSEBKGWAkDP1lD(tn3{Yg->n#f=+?h86om(>PvRk&uf@9gVVB zj+(14nKD=8b?Hb)+eUTh@n82%O;)gwmtTjfBuGEcinPtFDOy&ScNw<-V>6|l>&n4t zG@mp-@l_d-X)D-A8qqQyQQ0Gp8gL=Ll9;Ws^wk8#mRlL%1HCK*SqD)!yEUDy7B0N| zqUZyNFvRw1wHZKp17wQn8n*GPzWoIy`K@ag`uQ~`p<>^;-^dt~jwhuka6nR+A?xuy zTosET;y}|yEcSuH#mM2<*GF2#DmG#Ag@vtk|{8_D`1-INt4m$0N@W?`d=F&osdUQ z$Jw-hgbnA{_oU8AF{nzFJA4`L^I7X$Dqc}7UPy~Huk^mTvuDiR&(b&D5)myh)IoC3 z2T~ALn3U!^bLHxQV@zkvT*S)H zm2qgyH>X%bVIo>%xeSZ_Ga-WoJ!lpZYWrXu{_+|edrv0mSXka!st~8Y z;Wp1SQQH1KAX6cshhL_*l-Ou~bWu+_TIQ8tsrCftaPo?YD+oyFX)(xX>pNm)@HYdi@erZg zUkAPwSBO4U<@Vg@ExBTlBBHJe`(CX-r$HUJ+2}2=Y{Lr7EOxlq3PTi6coB5 zqM1lr*sA@`+PkFa`@wXxmpn@sJun-FCj};O5*5*){)ityp(l#{j6N1a5#(J86&J7$ zLM*w>JJxR;Q7=yfJH5On+){*>;R9blOw5RBgzJ%5)sU6hq8UK7qb>vk9Q_yXqzl2^ zM`_42&N3xz^^mJIa8Zr^2(s?l8)K%VdQbM4z?f zPFke+1i4hHLU?j7=J4$ubveE0Yr1*`EyFs*;=MrbqfgJHljL$AJ$p$!f7ZpEOaA?1 zO7hqC6KhXAgP%e@JK*;SAZKJaFlF~M;BN@B+>)O$UY&-jJ}^=vk7SLYP5KC`(7_E1 z5oQnTSrGkQv+jIV;5}s((OLI+`%dqwkx{VVhSt30y=ZOq6(6sc;BDXD||^)%RUkfMv1;ZV+@&`3{`}(~bWjR-UtG z$-lAE1dI&>0P5}k<5($yK}FBP7g} z);v|Lj;)VQ=I(WAmOd?sAOPZTBfl({Z+^+cpH!HmZL`*{5Dk*ZD#n!{S*k5GyYxG= z`{z+**KEhD|5VqHa!_}~xl}6v8zazQkh>=tfDAhlN1i#!`(15ttQ&P1L18n;QoSw0 zR~D|!G%mHn^tQ@oHfA;+p!2F*znSzo+(}Pjlb0kY27h>8&9hFvu>Qw#q@x0V=u)>= zKK7;n^&N>;DwNsFVC-KiCGsYB6`}pV9hgsAdX_GX={M&zR#Jlb@y6`w?`20|dD)}M z*U4*pT>Mw)`ibZP^P4!`^M$@$3l#3Dc;WBKE8)9_-gv34V|T0=Jj;g+H|mgQ(>v9B zIhA5$)<1c?5r2mz)}E=TgUc)JbuFyVTdi)j7!ZBTCTV&slO~&W@c&X!u|tXOYW)28 z$Ze7wdP5Z(U$|#c0heuX^uH8he_J{1La^mooiicwzPv?w*=IpxfH8T3N8Qo&7v$+2 z>N&J1CBm$MJVLbRd7J4v-(;m<(++$`JZ6p>M9z48`h%7z_d^t#oq^@er!Rp{YRU*w zTLpeRXL~SUb&|D}0S_B(u>U^Tu3uMbcS#8gNsq&$(q>#xjKQDH1o8^6!i* zGb}vXcyw(c=Pm$0oeSDB5W524E3**bJ}DjG_;XR0vjLqf>eFl@&2r%1(!M@1ZLhS(4NYU zM|T@^bR;Qa=&@A~344*gqq4k6e@wJfR&p^YsQ39Pg)wZ~L={HZ_8lwOxb=EpBJP7m z5nKVAXmO-*OiYkeciR;eNBdkSw+BbB+*+!V<%o-rVVVyRD{3T<=r+jCXhog1ZCJW{ z50_l9#y>ZZiTY+1w9(ho)mqodWW~uT+24q8%3=`dF9?V%?nT=KCD?ZcFaIbr>j-D< zgK(ygjEUJTQRX()9lo5t z07eH=bp-cDuVa7gsu|dDLh0MtBb}q^cxV+^;#V}6s`O`{6-g~CK30g%{8MKy7y%ULb`o?g0-+ejw(d!JeO=}wH zi}N)p2*@YO{Jh8~$*fu45;;L{l5lIDj0xdQ4}m&)q$;lc`Tb6H^*Ky48^ExfS@2)1qW7eU%fF&R3G`15xfK2Ev)6`6K+6-PY>Y)*X+R_oxFh&}?-0CJ6i&F~KLinAUT+XO+owQZ zCG2u!OjLHfC$oW)oLd~$m17FwW63-GwIty{<^U^tJ-R77Y zk8oZmUIRw}1xOJd9FqeDSUbIoo^=O!!kW!iD7=oQUfy)kq2BzY`! zYP{_qm(H2j*tTr>K9w;f=qH+CCC3vN_X-dYU-F?f-<+ibGtN4eKZ4OWyq1Vrd z2PgEyo;hXKTKy^P2~cj!35D8lo_7YdNxx63W;bZ|pErk|F&I-wm^bV;#P!J>99ep1 zO*ubdF5C@i_GsQPRD2hxLp3K`Ud%#l4k3Q!HD1eNNwh!7B*t_g)#;l4_;S^R(c4OI zhs8qf3T_t+M_#TOF_Psx0ALI9uy3`K-8Y8|(I+l9632WK+UR1OFKUc|Fgsb^Mtx5& z|4JcCRhIbdadqn|vDed^qvXPa##7$!!vlS|Qgbh1h9!J;;0`W>pSh~zjAfYnZ!;Ij zah(miB)jY#Wh*~*@1?T5aoJ-BwRP#jd%mpKQ_}T|=`LH~qUc`qT!({pZZ?>Rkj~6H zcL0R93fJBw@>u@fvP7y=qE@2}RS_@gf^Aa1Erh0_*6Nu`cCiIR)Ph!o8WRFk%oUFn zTUCmqVosqblIapFDaghxqF`lr)!7)W5VIhQjWSq1Sm8{3kamg%a=zeJLxaO$r73mG zkA>wQSR>skdx7OS-ZoIJN~cEQQ#Mzo{OaYowdy| zmjL@(PtN1(niH!-isJ_VtX1?s(f)rgo&K?g*SYTDj}M$~D0B@!*|XUhKhOR!E>AG_ z_Y)_xmK~Ac92SU}?B*xizLbxf-P5^Q#NYNKlM&*S7POUY#N$r#~jU#3BFc z5#P5{+t-4=RV(?gJW`v?&4WbvmVhu!!*1d|6i+e{|NibH<)u+o?@VNfB=GNQ=r9ax z&UqJkxW8S%KmG@fjqvo)gnIvu=-1hsLbBlO-)xQPs0h)fz}e^F@_~eD%-x8xDil5T zOyGZj$!f&=YJXw!cZXLq06<^(e*u#T(X#~N=qO4&{PVs`{F^W@qd&)wH`3ZWWBh;g z!h_mx8dUETkIKa)z&G177?&+6_6p^{uqD5Y!3}O-{X6MS zBtJy;k9@YkmATd@4cJ5(^5HioYHk^7?fFJd!Aid; z;a$+*b)uBbGPF^|Pp5>=#ieKfMcuq?8DD|8kowagyla;Jz^H*Ri zQ-x<6{{r|@d*}H6-xqburzk8(7bber#dFA85+N##$rcFs=E3Q2AX-u;e~xt@Z95^$XB_n8A4;R?AmhYP4zVf%k&x4}Nm?}^{`Zqca@v1tjazZ5XSRL* z*k`D+xNC(r28dz^D@(MXwbU`^xwDtPyUp1T);D>b%<{g>TJi)SirIVMV(KQrpmccZ zkVsv|g8!>=)lDzh<|K)hdW!w$eREw*|OMf&$vRuMuQ4C{%8Vt~y=S)b>pl25rDL0_gE zM^=lR_L{VdxO!4qTC*`KLZQiii(*WKIWEF4-ui=@>jz#lQ?lt)LIf}gD7BCd$4~UA zyGqK8i7Rfs3${RNy8JjXrUcc=e2&EsE%C z(YM%$m+gA;pc5Hb9tk*2H^_{ii}u{6o@TveS*iLPp;7<&8w(!DPb5 z*l*Xs20d3lf~AOvs%UIUAXdNy(1*LKn;4JfMdds@{vw|^?O2D`0X9jS466_CA5*}6 z%KPlq1qpV{cGg|98#Akp-qB$M z(9mxwz+#RU-g#Wx%8G`m683DVwTiUSfkr+cXi+rC+Jf;a5iNF;QbEOTAP03b*)a_7 z3#h2yHjUNIjqP*9;8=2lY=&R_q6M6%>yobB=^%Q=bXG(c&9fllCSl_&0=cL1R?cUY z-hG~Ne?ElKwrMX@#jE^HMUqw{341K(J!-0x_sLkdr^#>)t}A((pPw5+jxfKGe#R zU!3zjSPeHJ)cZ3XCUd`?ibFi8q_{l>*P6cXfIOVpJP4!IA|Z!r{oPH0=cXsg2_-Ub zuv&Y?jci4!(iR$_-)V~OZdqlaKF$+7)WCUXmFVQingmjo|JoVW0(yKG_(I0UNmSFh z+_45t0cPB1~(K+8UC&;;UIHPX!L5y2&tn2tCvh?$TS zI1vCjgxc!M_?jD z>U}|F6f62KJVz8qw_IGdY=r>ugHz-~7~W;}Gwg$U&=i2dg}Cp9X@%&?!fA`WhZdzvSGkiwZz;FOyyld)En2hB zwsJ~R$R?tEcxlUv%4izwd5NJJ`XH;@aoS50Nn(`+G!X|5h<#j;z<@ny@2_7>;4X%J zYljL*fTwzvU$}34L9Y11y$?$y(9E4O1!F9l2!Lc4lXp4M5Fame?0d=*d53ohM?MB4wU8dUPfpn6?u%e}5OLBd(J z!h390vRUr=c^n9J>G6TJc^Y^k^xRkKf}oFkf}owq7Q}Qc_9-#Jd=B5{TyT*x1?Aa4 zR-IqJqaq~)Qd&1l3Z}3|^9&)V$RuJv?0~7Hdyzd!H*(r(K2J4g?I{(O(p;lNLrMFu z;>-#UpB!F8;&L6+T^>NKe4B_t=3l0eApT0w|DwOpiNQhUs?u+-Pdgf@hz&He+g z{()-$-Q||HQMC4cCfBi2p?kwW^N5@!!xA4CC_FSnKFr1`6{5$SvAKO3zfJNe>^c6k zh5kB@|GsETef`GB99N{bH92mn%nE$cW zuhP1DTW6wHlX{apg}`-7ge_sHk!+wsTqK)NNmNzPf9ar!*W$syC@Q(@av99g(YH(9 zHHmg~CInr7{G{=bX?|piy(B1CD1$J1si^uFp}95?vUC`7bP!_;(Pn@50N< zYrp>A^IQ<|HVF93bIskp5+A=5#{cEHlUx2&DGx_7R}@!fPLsZWy7cxABU2;k@S}M3 z%W*l>l9qZEnGliJcFYv_KA#=h{{F3fte4)Vllug%pSM-G<*a(~RqB@n`UuCtg0-Rx zx=^_nvbTVo*gK8!Id9ESHOCn;1VY{MMFz&NThrAm^@Nx|FK*tO@m;~;Px;sa)P$c< zo={VgG6SUG08$`8jR&O0!>C1kX7m5gpMqal51F{~UC7(v`F+E1$jj&mthTq*&5o*0 zL?>1EK3k77U2mc(gG;5Q1%Gmk+bp##SroFuUbpr>ddPCaPzP?`aXV=*!kJSQJ zi~e?}r8grY*9{Mq3TvJ0^<&t?H)V$`erEm%;D1r&5pCxbm9s^^7uhbztunInq~1Kj zlZPx&>C;0+%3ux4T>)Jfly@yXJ=g?y^3ZZBuCL0|I3>?s`eUx6c%O});lkxI0NGIy zC41Fa>fSEH@Usk;AYByM(F{}H;ndr;x=EIFU-xG#`KoO0zc8p5kAH4{p&`lAa6%Pj znD%z3+VUyO=3oM+7|cb2{mI6x_M~24Q?{S_#?eRS`cyafq16T9RI~&Q??)y;lulX= zV}f)&8r%v*AJYehW5jD`Xe+Efmt#eH39|L2kJ)Bbt*G3YeCER~zie?j-*i=cap*`S z(laGPL*g}HdUH^x?_4KIZ{lM8Ghn1YXa`IhjNk-IBZAfThcjK~zXjegQt-uOw}v!_ zB!V)ngX%kij8;_uZB=3E2Y=bqc0-ch#+irLP0)y|B9aO*zIe5QmeOlXrHouNHSrQMMQzoZcbX6LWuq20t+eU z_K2=j+9Fk$ejGdPI?dDeW@VVZw{)fs)>BK8_9q#_o40u(>Y?eJLCYnIKo9@2! zbrf^0Pey;H6oq2f?42@yjs}wgoG(-e5I`z#8~NmT9HX?C-BZ0aX22_{54K{{gB_fvqg+eX&Lp%@=SM&#nHGBTm|j>=tUn!EPDAD7%mF6 z7-!y)h7Yqeeb>wTZX!QZO-O2ZbLS3c|8-+7`)qh)$uSaO$HB{g=xO>WvcA|^-Cuvk zm$E>9Fs&=hYXlQ#8m1SnIGynn6@nDkKSFz(g8AO1p@ay%r!Jv^S$Sj?GWU;k9%4$TeW;aoTqbWt$xp zo0suK@i%j?4OD@p;$B}R8M)7&PiHjA{tCv_SnjXb#Qh6#*;X)pDU)(zhiio!qn@II z%9rvk@Wv$c$NYxn{ALZF04P8zIkIu2{aX@EI*#U3Z3QRPqBK znix)xr23JscLI8OuBnpgD_=jsdAL(j#e=9n1>SO)=+41=pcCv}nn&kvrAvv9@$e)a z&szrIMDM4b82Vrh_*4S~TrPlEX9wSu8zQIQt z6?KHUhkfcg;FmTArh^9x)+bYcv-VJgnEU{`4FF6|9os<-mgL3ey%;MWiS}w3n74BT z{9$&#s+r4!<@%E%D2lEgOnT?>YcS7`C`1I!%bbO%lHbbIA~-FRfrL$p_CD>^XJih$ z1M{56Bc~3HOWk)SC;`j)M#&hgynoTK_HA7WkHf~ZJ*k!^}J z>MJFM)(plwugudKW|RuP6jjsMIHkCK<4)H}Xu|h=bQzL9v~1|Mp^wVsShJ+zqC_lb z$dyE@U%=a6@EgPU!I9x9!w*J?HVcsOg)VV3q+8RZZ~>x23=tZtc8Y#q)?zR=dW~&b ze^H3hPwt163=?fdV#Rz+LBWRiz8Iot89t-$YYEC;k?!9}lC8*Aql5x|;y)ADt+V8v zni+C8C3XdvE&@33?X+ooQvUwqP||690S zONa5*9k0stJu|lCHf8xdS9wuw7G60p6{F65VVVu^XIiBfil{qct)<;+8kc&!?z#AybXgUOu2S!-azNz-=H-b_By=!Y zYI!ro&jeu#-nVOTTd%H@AT6h^(@v9m#y9L-Y^S`D`Wv!w^FR^v>IsSYx8-`zZ2v>91lDn^sT~wKwSR_+~#Vg9CpDwNs z?GzKJq$m_d$$jfyQ1#T~PHvihC0o6ADEx}+&R+j{?B0H%i*z(y)7DvL`Hcx$RGrM3 z2$s4PSfg*J1sH?a{LyvatQMJD>jq-Q_2oa+fQ4j)0{lCl@i& z4@htE80E8L7AjwHJJ^VV^0-njrd`J=KVG@QHV}Hr@#CK+1osIJ1u7+W82L7JLW`W@ z_kN(2DDtq{tm4%p&DS2^8^~l1v#t%C&58t-WQ1S*m2yjB=|*upHR97~n}fQ69%nvb z@BZPfv>hbjRpt|#tF(=qLi_E{tDbvzZ`V6`%=A-h*_D&=ywapk|7BunVdEj8HYA1A zOwS7I3+QWqfJ$^1QC9t-ZU^@>mk|}NPg8EZ=Wb_b>s5GEDl4c9iYB)!KbW(G>GifN(^X#o^isjNpWqli8@cVo*A!A2( zPbPG}gRY6<6Std5#o~{{S~eRf2Q;~&sEy$=d3gfQu?wPK_H6C+^uMi(Al(MWJ$@`( z4{ImFEY;Qsi1(vD?E5oD#bW9UCMK>B&v@joaN{i1ERcE&=FOArn{AJjUe|!TL!!l) zhd)v>PU3t33%s!WG@(6y=KGJ`Zr_NPQkRAU{BzrfCnghYzqQ(OsYUMz1?*S^NG83# z9tFB-cG;KY>lGyk_`{W~^?~sLyA}J@dNzKFUyWzuH%gA3gmz^Te=ufRafH!k^UVur z02lhyIeYv+A$?Y!Z)OhtUgL~g6b~j1?2NXjVg}OQ1*@&Jc1csFr0@XKm+bCe7&xd& z`oi-;5KrX&s<|^&l&A`Q(zi!-HVicnz6T$hGa z2_N!Hr;Kmyy1rJJTj(w#bAOtXmtkye=|0jT7R&v{Hc=&kao|oxp9a&1W=kp$ke;k> z*!4ooz-zT3y}@6K)5&-d*wfpa$q ztB;@E9W``hcaEWROS$*r2t9^Ui9g*RgZV3@mtjn8hLQxgk1c5%8gQ0`^4_{RLyMcZ z{FO^l`QJShLhl)@Y29Ot4Md1RoW3`TpQVS*tf?g+`5e);dEqz`*(>d1c|S6DJbgsr zziQAM6&aSFfeShxbs8S$fvL^DpM4dDm*z|tbeyP&O;4GSNqKU$f4`?9|Z3V+mRIH?e@YP)i_mu6+1K7M`;GO zu4^6aR+3noO46}$HqnFGw+MxoR<3b+++m?O%rO~@X&i;ARFc-@_|K7v|-@9XW!@D$~ zD+O1aTp45A0+SV_SS*~Q)3`e=A3EH==@@Xbv_|&8R%~=gbfT@7J68pX`7;x*=OTmY zyHho)rcrXaGO7mTzr~2-HhMdm1vb9n#5AuXCR8Sj9_m1BHoAU%reAR{LIn=x?(OO* z7@=YU8U~9-{@mi3v3GkzrA^{Clq|c-+YZ?EeMMVL?y7fnPJWaWvuuDqAopb&Z{5ut z9{KX>9y9kg&xh@9vXn?$G4e(>wU9Lub#j;LFxGU#Y_p9ruI-C1PD;-x&|BUUkG_92 z%1|!8^>lCT)lb#q+wa@LMsvML1|RRHBqlL1p(SpL!e_pSV2|EGraT4A{qDisZ_OtQNEVEWIz0c9YxjZeCx!jvODOg`D|QCSD=bjuI#(yImH7=yO9vI|Fbx zExHJbPF2<$k7Qp1hBJ>{opx;zyONmQ<;F6kOePq7HX+_W=gZ-}X?BtYb zPEZo!dVxUfuW3YXOx?hpGuqp~aZ0bt6w@IUxz%s@i+vIKjn-L#?n@V0+-)@NPvz8X zKU^+p*d$PPz>S%(af|PEQz!}vOttSYGDW1{GiKtcL76SZvy-bIt%6jO0+&&QdliQ} z=|_3c$lG2YTE7ao0_utw)W>pV0=C+JO~S(8!sW*E4g7RDep=xG%WxD&h*Q}|OVS_+ zwLGM|NbeqIVSPOwou5V=MJ>`*1GEQ|Pq=dwzDk?F3i00!4iHOfRsVEpV|5>sKA`70 zAz*`U9#Qv>!byZI_Fuk>pGSq=n&rUw_jpn82paL}B8(X&O{IX{Q|w&%<-1#ki`>vx zG2}9tr6dK@qwI=<{c%<**ct>*#?TONOdTL6zzn{jeYKIf_;DD6R7wa;tPG$Ha}fJ6 z$DTW2*$cnH>q_`^_pwPmoWK?7`%Yle3`ofOs4Wo%`eIFwSCphy+7YpgWFk*yYS?j` zTzol^O){64p_LzVjAcE{#&L_1UJ7E|CHOD5lZGC@^L;>hf%_D$#hkTp+3#{l^pNW- z9zkoXvNHvB%CPWYchbID!iI6wD1ArFZ~Su=EcX>jtl<{DpMVENC>PtXwj;%8Yl2y2 zoYYr!iY=Nd#|-~4_uW?)AO_7NZG@$=+u{E^%NZ|Y1+e&T(RL>*CTnb5pD;0TPBVpGn#`R{WqBcSXcFZPQ@V&_M zslMh>4acqxKf(}3i4hM%pe*T0u&M@>!UE%#5Q&f(kessLo-kd?W$yA9M`su*(>wW& z6oS;$4#>tY_>&$OKj9eeh6hAr`^rmETPL!oo#ohB@&mO9dG2So-S}!i4l11b zWN-W2(KRPGXcjH+N$*dk(w8qZ2u<`#{OO%^OJt<&2>~PQz=HXlu~xKV(9JatxL z!6J&FeT2#)2yXP)pQ(VTiV?ghugu{%tK4@fdkd;ap~|I%o(F!( zEbOL0=ub(HShCnoF;YIp$J!L(e#0!Jxb3R_-|-2AHZZnvliO{eFy&fCTcD7$n71~o z^?UDx7^-5L$j`mdjpxZYPGt2ZIr#I(A5BE|35JrVG9avfZ=nM}+g#5O!u4sTP@9rh<5R{)>Mv!^TRYEB*_DX4|;UysQdRX)# z9G~nfeM;{U>0u_x)t4IWs|eKB6zu$(8gEDB^gQlm6XiA1fE{TXzn!?KN9*2ojR$Ve zxa(WqlBODQ38C^+{c|lXqDg~i7^j8*u9JSv^g}(ng&LWjWRsxarcJ~BWrde`FxIrQ z172TL?={4CnV#i8OJnzh4)*;P{Lwd6AvBFhUAuHQJrmKYxmD!eW~{t45XUa8nDLS>#Ea!T`uOaYFQ+>1>o4W#;&p-nm;cg z8=&tOZmt1^T_S0C@-|Z3jN$lgh1GW_Nv?s6WlDZ!g9R!Iw_P_LLUjM1r}2E*0n-&N zQKSo+UP$_}-~GsgA2^?c?~7APlhCn5HU5t_+|0oh@>_pivcH?>NY!cUkRwd{`@m>1 zdvTR683G>sDUEdM%^$Ue&Lg5tipKEOM^p;n#hs3j#bk98l-8donH@R}((<6DgVc!J zfe(3mYy^{C>pgrB^bHd)bWK`(77v zPP#lv1@uWpB99FhRkLt}t9B}I(-t6HIk+-bHPlX3*Y6S{(Mg_%*KV;Q0{7bt=tAw7 zmro;tnFI-^WZ9}Vq05G5jFRjjYW$!c0eEr0%WqRnalnt{gA0F(&uL>>BYTva5Fr`% zW<@)}Pp`^7o;rY110SHi&eCZJLSk$|TG7l{T`d^d*aY+4iKLq=r8)h96WKKW_@ew6 zWktv>Q$Urpq5GAfFfZS}41Kj2s%?B+clZohcekuO&-D3v@J|)}W2fLj#N=++gS(rZ zJLOVOy&|5Ka<6$xKb&N$n`4tm*vY&bqV|ARZE=SI%N`H!yING*@OyG2AjB%LPuu|9 za1enAPDy}Yk45P$aQB?4%Ja6abiN>Ox+9dv>C0i3ezWffv}30++i^<0b^>0RN|9U} zp=P&n&)~Hht}_OHz2cjX=u9HQHkJvis{sQQcJnJ{-U#SbBjtmC>o9D!AEwjd+2@9j ztv0pV@uxt>#fKhH+NLZ^T+T>USIex8v$|2syj&7K8PLOvRGCXot%w!`6S+}`szR^f zil(~St*V6VK*U}NAWWmR81oj#J=Ue}v+2UHK&J?ePN1SDnAzU^ks`9Kt?h}bAOJH1D`;rYJ4&N$AQcJ zcGgcN?af-_aIGtC(MYfGhZjisqbdP}5_UaT?g)k76t3A}ugtopwMCx{l8jo+13h@< z4w{_%X7wP=Nm8R|D5KD#bTmzJ(mJLv%+oBZwcazR+57tD(c&H-obS?op?rG8@5inr zyg9Z>&x@0iE)_rTz7(fpZ%U>&3Um9s0T)75^U8Vi8d2|OKQOT0uj=G~M|$-Y^pWXH zqX$UX*_(~k-=FIxcfZ)#d{>|Z#rUtZ`?L*0XSda^b!LZO=J6z9erAg=d|ET=b~%rS z(W*^_vzAf#8uoU6ul(JwjKV5JNZH+ZP2BN2Halb$&c>*$P04ezME^bmcZUlx2SMl= z$S}Smm8sz@x_mb<^96ovWCVLF;7Ub^J#dSMZ=ZtVQawDO&LD<5;IVQOqS04Ep4U|b zot^pXZ9exc9+7=k=hRM)uxG%Y5`Aq)lCT7t`~PX(%>S8i96vs@u{km~g^k?j$dRax za+f_jftPO)su*b0)Xcy}NT$AoFjUjf411=xX&5Za2Vm zpmQ9vexgJPzLV~9;1n5KGhBzKab%{^q8e1lKd&3?Mo{NOQUl&wXOkEVW`KU9SKkXd z*paGBJ1d=|-G=2kRyV4I^Il)>xVene`4cd&+s;g%z37cq4&7M)t>N{zZeq~*!vZ|8 zw%!euYh+F&2bw9N#+HwCwc;7hAIh<5$Pne-h+u?jRBTlnEbficT>W#_6_pK!#06s3 z1T_At(r+GjB{g_$Yxt7pOs(FO0X=oED81c*>YA6A*P0@r6)c~l;hXP34z0;W`4WX3 zQA~QwjWnN*5l0Q}i)4&a<=NMPk56ygKs>NxZp0$a42c$^kL#o9(@Y!hVK)-MBucC> z9hwv!qH`i}sZo&Wn_w-WrMb$4ObgFZTS1M7BYK30E=K;5d2hkl(tkmF6t z$2@m9G2JkhFrU4*SU^aacOIeOVv2{_w+!P)E=1&8%S*&YE!4DB9H9FCkj`JY7FPiT z&~E)|ctZEw>PAw3Ar>k#D(+pHk!fQ0rSOs_=zia3!US#)f&cVe5MHEfH^~JJQU`jY zE1*!t)PIBrN!RfHCQc%eFt=~>Ue~}PO18{8u}vwxa(;ucQU9Hap48x`e5p1%LLH-$ zE4gZomL7omb^284tXFrk!Uto+i(vHa+Xj9)O>@2Vs;%XG4aVLLUqnuaPyQZR)vhMu zLNi!dxE0$7CQ4kcXQ+djKy?-Xq9y;5d(uhz^2miufhH&15#?~XLJkxAvGP@B^ zf25Y(#(s;(VMC8KYhiPZPK_HIc(>m&xR@I&arZJ-Qem!}uNgC}W2E6)4yac!j#7N6wdZ^Au?@u8vregB9W*{m5P$FAu9AR%O;ZyMGF&@j9X24I`HDvip``r_h)GSg7@25~55$#ikAp%wDA(jWciX z=Wy5yY`+tB-M3?29`qT0#hN20k&JnL!aZ$+TQ=88IDZE^`JQNZ=!Bo>&z)wa3}F9s z#Za7;C#@Y57*&w;h@s=DS1~r;4gCYzSr$uOC93BJ3d8rl$D8;~4F5dhjfyM!%G^_X zTJPnk#wQ|C?t7=WG515(W;jI(SFWAiUE1Eibd4oh&wEUkBMi|0@-s{Qo^rn<_k-6$ zJVjoSZ>-1-&%dH_Rj33dO>kKhc#r>3?;3V9Cuddb766u5+BAo$)b8H|V}e@f3|?1{ zdCfiu<2gcYxE29Y?7omwvu&1z7cBF(Wj1Nw_eg|e4Ba#1N<{&8B<`ks+e7lMsyak~ z#K7*kvAq0vPKw_r)M0X5!>F3FbWtTG4IS^lZFV0G*z>`;&(Z{?ZshiDGI@zm%VW^b z4`ryUl5DFFYLlU{VsbC!E`abE&HPW(B~5NwTu;G5k>1$ z$>}(q?XB76F#cFQl(y_ofeMbz6E(I#(&Dw>!C2qIjmCoCph{R-T)vLBIbSiz>~2Bd zsbQ4$3k&EZ*-jv-T zBy8*B+J*TnwKb}#%%d~ATmpXG1xOg>tHc3D2qB?9Ijs8(=Nx%-39fjlb(T9lMB5Oe zFKzH5=V9V=fV4<3(mU$UyAEP*m9_`%NjqW+0LrLO((gNBHSpL7Vw`nfpBZ}JIL3{; z*DA#GHQ5=BUn`+bf&VOd3lqacVO;lVF09&nCzO1!Wb3mZaHCO_Bp(`YS0@sz?%O(> zccm?i^M<}{3~#$>&E0%7Qhn9;&6@Y%)llEw<4#F&`f`6JNuDXgNPfr`q1txD_w_k( zsMg>yx3m!i#zsB+_E=%+98b&DiCX{fRav_+5lv4MJv$a9L1l`V#+l&Nt^M7&-EAge zE+8mI4L4vqUeXdVZI7Uf1?#SRmTiArlmaD?>)9E32E!Yin%{L5UAS?Asquz?zPwUQ!Kjzz- z-!FUKkBMS};LRcOaR*GUl$Cz()-M6Fw$hXGCF#sExcz43!8*)V88PO#ANqtlT+V*tyruf?+Ww8 zTnkJ1e@rjtm3DWmB6Yi3bcfln)ek2XHC;DXFJQwut|K7&ln9-lzR8~jV{(tJs>JcO8kkpP*c;__E+U-QkMM@S1-84M%aTV~h81bI?X ztmWnwV=uSV5x$y(qFq?vEWPtQ#Mvhz=F8>DOc;Y-#HSQrc8At72Bx=)ag28HBJ2=C zyVy3EA)Sl+iSNV4`MOPRI3dERD;TN1^~iVhwNzry)YVZh^B8EYgQ}|P-&+>h+V1;e z7{n33A3E`}I5cg&Lvu~Ur(WtE9*KE*sS;J=?}K8z74zBFezxtz*7ll3Q`{#cM__%F zE(jg3t8q6Ju;NtXBf2isoE~@zzDvEYY+6px6%*1Kpqv4FL|~?zeJdV|o_#9O2}%){ zoP%HYwlS4qs%Q4!b>b93e_z|r>j4Yf2n!u+L89|l!p_=m!DRO(Fvh{c5k0-&o7lVd zDh?LsIFiH;Li%>Gc3~EyoXzYBQel1rpMJ?y1%+~eWW&X^W9%=r9b1#8zuS-jx5#E6 z$mHz}ZU^)1*p$FsMJ*P>L`^QQkz1uGZ01BLcRk{A%@kq!W&3T&wQDS3`cB6cMKf?_ z%tWRnhaXWF6WtfbyE&9INDJT+k!G5D1TsBkN6_>`_e=AXl+MPLldYgLlP`G|>wZv2 z_++8}5>qu3y+)aI6ZQU8rId^&0SxNGKmbz&lv{`WCZWw3#@2vw;qj6a6-+5V{kY9K z^U3G39e@Ul#-TlHDh~i*Th7S&x4<1(OIgHQibndH%QZ0zEQ$wPIFN6kt4pc9>)vB@)FyO zw4$f1=bzKAcf!picp%+zaI{m$d3)*(ew*6W8juV^celn3bx?FK>IT$2NIbXthmxK4 xa0d#~og(S&@xmbD00;;O1puSbKs*|e0zmx#O2dnE#%0u!;X!6~Dk;QUx zk*5^tEy?&jKQ+5de zfbgnezG~X88sLg&;O{bp(d1{p1ED zL9v=k+Qga<{;3Uh6c!!Y*<2bXhd2@qHw!o4C;#%!XR$@$ZIZTgV;^rak%jlfM@v+b zsqf5qxzv`+yC*9vIAt_ntyisY=2U}w{(H%A4YFY>*`}PD#=DEC(~KqWmTlQqx&2@% zA?#x-gg_eri$~2UikO_x9VQzu>}YH=VK>S$PonnKZTRiQr;(3eJgZb^grq$`ZFr#G zn*RrFpUH^GdwY2>?Bq+5TTLT=8e!X%{ueJBj zYR6FNEqvr2%fVJq<@E>#^{Ur) zOU2Tx25J(#c#`2>dog+kXi z`RZ>Z__q}Kk;i7mI~MHS6W*j=%eF$%H#3??p8I@1xHJ;{1?np6hmh7&%MowX%t*@) zx9^h9gkgq2T&p5G{Kv3@N!Kf_?Tq$`PcP?9bIpe-`X6v)E0ui6SiWKDdu+CgjA14G zDfUaViY#H@EVq(BqW7Jff6fjfbcbf#6bcu54Qkmb z6HWRX%ktCoQPLX@P;A4)$TLrZ&rQk5BD?C=R5t$FoS;CjA7KB zt>I0@I(&i4O2p2ti?DfSa+Z_ft)~zwfIV+d?74-Le;vwiWp}DOs?}VfrI)fXk+qy( z>7Dk`lhe4yiq@gsTR`65>)$+yD;HJg5H6imJ^^Y5%5_djIqe*MF7s2~*ygeLaMDXd z)i4MTr~rga0jG}gUlMmu=saV$*HPwnS>m#Z!vVMb{oD>M6*&M_=3Cdrv+3>;elXuQ zR5MLHT8d!?+--B7gi^3KwPfDP4g@Eelnb+F{=-+z@^=y@+dtHCEwrETpt0-gGU}U; zj-Tm0pjWqu)2&biIcyW5NZdZM)1>&SHCCTu;s{$|3taILNQ-+MT)n4;a+aod6w+$h zK7nu^mIfP+3#rlhJX$cO>e#!s{)4HZ{G?Lr}M5YEOkbgdT{;T80+-j!*L@bD`J)6e={+z(utdu;OkgN!3 z8E1x2!FCG`y9|R*)6b`Dr^7nBIG8BJ=Qp@9v7avZD3L295SY6Tce+Gig#*zyU(P)O zna>vQVZ8BE(8kNALzgloLJCU)K~qU8nCOdlV@q9QD7o+33Q%;e2~yYyFX&6#Xr_cr z`_&W00T_R`(De)Qxc!4lWGg=9oB6hhj7x?__(_@Wt)3Vw;Gky9TvWubd-71B(mqyl z-kfP>O@yMSc3^u)Ix%g*z=inuE-=hp$+M(kGj4H%zNw|ULv4z|AMSlLCN?h&A<4Ec zG;A{DJIgp75gAwsGvDcU=pSvZd~-pmH(JhTX#)VIc?#o=bFD_($>lvNwe$H-n4qR| z=!t@nP|f@EOm6!&pkVc3su7#fq}wh7+t7p1(=ojsOw|C7N&0A0hm^Dp4Pc>TnJvuo zJKVXkuq4aq^p-4MNE_qlH*w+buc1nA0qFl_xHt~*IJC($Y8>++o;s#UutAzHQ$T?2 zG2(j9zw}M3Y5V_5i&t;SqRpgAhZBN~cp@c7-;2+VfnCUNw!sPY^1h`h9 zI!v_lfu77Tq7rTFhrL&9`XM{6K_Ki4KmxP5yNo;%G<%NKGJA>nCt|DCf|~faSW-0` z`eMw=CuM;YaJ{ZNU@?+ZrIWq@|BgnlmbwOw#!J0GKR&zfa%KaIGE&0Kpz7*|JBPOtNcR2fgEC_r(`(9OtMCfm|-Fo}u`2g~g&*uD@}g z&x?kuWlpZ#Cp4jW!PChBSO~sB7$jtmK8xsYx)r0@wzhjdk0CAcgpkmIgU)g(9PcOm zS@ib%VKx8fz}0v~OvTEvgMk1@uKe-w>!?0)S$e-=x7f?e7GH1C=O=cruv#t_?>I>o zdha(#em~_&Gh&*KYLsS2$%yLM|42pWKW6|e)K~hab=kx+IFmcAG|;%+AKv;Ci}T6X zAXII%40z)EP4Ap1+@(l&+!GDEz4R4NDAyH$Hjj4z8{3kf`p1&Da=0kJGI&?%Zr$wH z$5hnqfTqxK%rmq~t!RR;f)pFF^m_18YzR~&001`07)Twgz{lR&oQ=1lf5U@QP2-7~ z5gRw~?$G)TOQf#xj@RW|z0hTDZX5upOq1~6WKyrf_D`P3f-^CqaSy@_NM^jk7oF9W z)~ekmJy*QPcLo6@=r^t>8^o}d$X=9{ry{B)(>wqY3&Ac_5Ozg)YsTq@&ciMm0AO_? zh!Rv48Q=7Kd7e?H_6T8M&%qe<;WE%r^7x=MKpV3%@i8GCNqK4HjSGPSFlB!IZtsYs zhihVH%^Ozc>cN%%3-*I%KLPgLm@J1ZykX(Vsp(Ji)I+{)8X)^V!(eUK!QSg)I&6|N zw~M6 zg+(gf8&sSG6;kh#_*&`kT^#d1i2`4oz4I>&vaXajyU(jGfIU*>(M-C=JLu7@h@i;! zYRm&F#I)Z$(j`cZQmJ~(T%1>TjK|;C0svCShJ_z4s+ z)nW{S0zmg^ZZqlKAcfEqZk!;eArX+BdXHEa|0}$m>pQl+G#uKzQX9~U5F5fsjEgr zVzURr`=7u$pL{e8U+AnhqCz;-1mJtZi;=$SzWc#hCNJ%>yghxb9bar>!;dZrfo0v@ zKWZOZ)#B`CI}>h6i9Ww}Ev~R!=!PCpl%Rd4@tzO2_TKt0x36GXSwugTeFi6}`o1ER zUwmBRKK($~CEa*d57GE$>@n(6dSdiABFnjp{c$cPuv8jXwkPM-PX4(eklug0(Acy$ zv%j^a@ZWHt1g4mlyX;}u$7^vC6nia3WljmTFyxpQzS|{f4j;3POXwNq0DxrXYccDI zVFYKTHJ1|`yXM`G3@Py=Z~(9bjQyS}_ z$ScI?37CC&s=I#B(LXG|6|mPo-^@t3sz;w?ZsGZR-c(q0kin}{?hgRCB4n-1A4HY8 zVoKamr=ueV`$OjUvIp(;I~w`hR!<^_SeH05ZhyUl2kkZ~J8?|zN6PQFkb3jyQ@HP$k5!U0-Q!`Ym(F!2jiEhx^_iTjF-D+AX0rX-z{%v$Y)$E&Pn1dGK=C4WGCN1pszsW|&9kZ=!n5@Aiml5|}%y1&Obw zh$~&1^*mBuo_|m;;m6wCZ^1qKweRvtjzAH4SWi8POcB47+HH615(V20gJ>or_~DmI zb-sQ!D%kk$*hSjBZsCdRrudTUsnMb$v0_F|s)t%qh0KI80!3xN8mhlOYUgO>T@*v5+W#0`Hyjj6#2)T=Dv6*?4QSZBy?X{Zl^E5a$sgyVfIT#4rd0w!E z?VG$b(M)IYnK;jQ`ju0lq0`NZEB-6KHpdozKAC*l>(bxNB7!$rXqlwy!jBI{bA;X8K3_I6+K&s8e#n>={zjQ7Z|o|ynhy1S?fcb#cPwFKP3Yin zEPm)Bv5*%d+6?7w!lq;xi%1v7n4V|7Hhk%4$tA!Lk$RNFg&Ee=sgwH~S{4GS-^dEa zWRKix)3$&c?>^ zvnE)=R`!oxBMV$SW{algx?cu7H99Jze3+A&(mH2FQ?%OVZz=s7azDyXd8wXt*L$cA zX-ixg`|Hbj9V#kq3vche7Y=7TeP7N>s~SQg(PyaCRDIBOio?G37B%t8O_y@_)ue#g zB1k6&5&$|io;%#K32UYI5 ziQa0MUkOF?0-_i3`K{|r(HH8{t>SUD)F+WFog$wIpBQG~x*s3dqIU|LH@;Z1=bJtF zboSNLRW{&MwBE7}jrX{F_D`gVP>nX`MsEdiOqI>7ZuZSrrV9H^c-Q`2xlzdS+M}k zl{*V5$7;jHUJ(y$W==)RdPNH;Lu`XD08ZbuL5;gt`R7tJNt*08}AvID}(h9a}HOI~8p54p2fNr4@{765#dek&V z(W-_3rpqFb?J0ww)7xHd$9e916C*;P2)*Nrq^f{&k&Df>C_#-ai{HR?lHTWxoJSh+ zq7LpKF5SR`;4393PqklHe@GG4K-%Bm^`&oWF1mhb@c{6X`CW=va75DLJZT~2@LJ~d zEi>15Y4A0|+?PXCJSZ!XfkblGAiS6ATV~zbg3dsvewV}Izmh)M;?FDvPc0RVv90@S z?)@$gzti*J0KXpn4%|g?R+`Bz%t7KGF0yB{ zd{#ON(-mLNFt|Cp=d+{OV;>C+eq6o4h&`+l@sedT+KE0R-1ZC6r8lryjYn^kd3-5a z)Oo-uzKK>zm>Y~V8)@1e(Vp-%)OlnDuHHm<5fK^3Z#1le&OeJHG?mgkKWYt*@1lO6ATf(& zs;lQs%}{?FKstIazLoz1iVZM2Dmrg1{nRN@l`ko4PpF@=NvxXha_CXCP4zHk>ibxe z=(@)vtsO3~PdbI`XW!TPNv0lNesXfD{PS6F7#`FZyyj&_P~9!Oo~w9_?AL@>$u9I{ zY)WZdJ1LAlB}N1al6_gK(0IGx7)4_^RY>r~>Sn}&cfwtOW%9J?F_lpa*Q*qv99;^D z>hHfwe%5ytl@^Tk%$qq~S{1Rh{me@GH%KMD^?PUkJio4MP}eGl&3x+i8bq;da+V>Z zSWRg?>_J=iKZ^VBCDWCGk{s*WpXc}P0fq?_#w{nG9>)z1ehVRAh>55UY4wk6R;inl zUG%K+bHlJZn($22>YEl3vnT?U9fQ+f(-`+%jx@M!t-h>$L8AP$?ujx?Rf1r~6fvP8 zSlXnKM|_`NRr28(a!p)=8fcqmEPY#ugAX`{t2u3Q00mDp2&LyQKA+SVKbi611w03{ zRUzA+kK?&7vO*ucFd}*`W^CC)AT6=kRkflnY$jo>r72?*=~yzjynaCk0VEST;HEimEw{`GA+U5^{;UWMql}=awhX-%oD4{S3(CuXsV_!3uEZkbw6O ziw)zRj|(CMm}PVSc))7T*o~+aL2tvDpFkH%ecgx}3}GV@BZvWhJs{>>o)Xa95b$Wb zP$pWixe)hx>Z1l6Fas9;1A`ZdAo2hf$kw5aSwFCmKLP8oZ(ce+Pno|!VpM+rsL=$_ zMkF886ai);@EO3-bSuH?_9)l|sAdoSdG*adz4l*x{5<-9Lg*n73xM8#T{q3BfAl^u zE@cT!GLt}_=2HPBF6R$-G-!jhKA;>KMP1i6GY=zUfY z&yrFB{5fBMW|RMd@p_Kg1(|EY0KlbIPjWXh$6}o@n+Ob*ee1oiM1QAJI?diZ`)dvP zip*i&hD=o)Aq+#_Z2 zeOHF%Ga~c=B^eSD1i-@H>nyydby0>}jLldw$M4cTLB6*(nlwD{u#}`B*Zj9%5h~bQ zbut1~XvGqu70Pmp|9WpZ+~6AvX&c>nsOE8lunhpV%`rBlj6|8|6Fnq7__aE?tjA@l zh)vyw3;OOmYvJ1M9E(jYL`@dHJbJfvS4AiC<$3GU4D|eGd}I1q&yQqaNb_GU-M&!- zK{rUueNAi;-}Xeddmi^S3r!{znlbiF)o6jgwx6b6`L6u0EV|^3rvASC*0b`!Xt@W> z0cgkkTXz;+%=R~4krE91D{%lM2r9q;;Q*I`Go5(L!ufDruq~Dhr5(+mpIp|?5gXHV z{qDTSyqn8(53z6zK8uC&ea&GbQo%Fv8`<_3G7?AA&*M_p2nl-W8yR^$U%~4R#Tfl1 zUtl14b3`H15d&4;znUK$!UG!Z^*43;Zx9dD4so%7S%tQv9h}WqN#~!QS0k%nzQ(dC zIeo!(pFW_{dXS&3s~tdvD0J>n=UC}9xcs|9N-gzG&*hJNZnleg)A#p7fipdS`C$=U zcL>1jOY#9ZTpMqeg>*c2AHz9ANbL5kVe$#v#POaLO zz7^yRk2^-j<%6A^GGCujaREw9h#YDluZn5r*zE5b%~UE|7JGnRQDfy46EzV53FH-! z9IM2XT@-EQNjD{26~Og`{$pW?$__33)-IJDwGslDk_q~4fN6TM?aaQl;QB?g??@8y z#GhB`pr^aTgSX8Obspx$MCQ6i$R7vDgl<{@m1&TL+M2cW{cZN67?>t^vuHf~`n|HQ+Sr28hB$D}P}kBktM>oS^PG}^+G^hf=bXvJy0 ztDh_DTelqm8jH=hGWeTKi<3K;jI%8Fi|q#~x+(RC5c;6Ct2MzHBc^Ff#&32W#Gc+l z{p;Lt0=BIS}n4S0LU^&Ua#D)hf|VOxP43=p+6L@>^^+gX|=qK%=HxDpQ%D2RH!W z5C9+zUH}074uJ5m1Yjd1K+J%5_^A-vDMAJiOtV2Wb0!0b<}$}-6BrRd|5P0@?pNs3 zaJ3;GWU==E!|$F-&Wk8fXLA!*ppKZu)Rssgt~9`N6n_Vp*9uPoK;!tV>ECh&ff5C< zu)s9*$+1nm?J+e-%-DG(X=%!ASUWn2M}*(9K%{nHq?_b}TGof>wvdSw2Q;`bbM zwZ5$Tq^SZ-r`aUYAnKtz6;KMWlU9rE>v;Qm4b(nGCn4tzC+ls5=WwBC#r(CY9uCDK zlc527Ml*rO$eVi^v_4yGYoD|+X#+&hYh_-%%ef1@2zc~-(Zn;hXcrUd?{!!KpP7#A zlKxTj{4Sk}?}2r7j>uS1shxg9-1_BZ9r28JRGP*+^B2xVWPq(x@tZMgS4IGqHx0xY zPol0glr7hwa9u0?A9bk)Hw}Uk>*0B}8JZv5EW11P{7um!tdyDPOJOS2AOX;8P0}5N zof|~O$H3#VG1fgZf4XdA2VP$5{Uz^8DnAtc?|uG7^h}W;yY(=7X|u!kUyfCO*GH5D7N1%Zc>_?Qut0CeL|@Z{ zy>V479%f**yw?Okh>A=uuhq0?gY05y-;f-$o-Dv%uwgTgH>~q&M zkAh6fs`RO(*ovD3CrUVTTg(xeHlH!rfD;n=-t!sCbUn!q<`~C_Q5ytf>@UZ*}v%?;+z zID3@ac8+G(M3;p)OuvjPO%!9(o?)iuz@zsOOiP@M-+ctg81%EfX;3P&*TX+lkgh*j z+4@zC8G&u)EIH7$oJ^zT+@)+b&z8Ox(}6(f-hqibJ(NT<{ZS(V<{-d(Nr_#B>k!?P zRKhkYAj%5pE#BRG85_h!;EIs?%=F!c6arap+@|7*iDdhEyKOszxUDHR;}QUfCf&9m zR9AHfuo1%liWqdy(U5FP0Cw*adA1}YL_mA#lsmT>7k^8C!t4Ow_jEuxC^RXRfZ#uW zP4_&FNS^_pvRN6=p$qrN)s3YbYRY3Y}8o=}472y#w>VItDk`)`+__w5Mc7{fFlM`cx!v`&0r+~=U( zhweW*79qbBn%d{%J|Py<|C;m`<)juQ!ADZ-mFFwv>K z%6{eF0U)>yd=*3KB;VpL^GbOF@VPUAPO<~tr`0vAIXTfl18#spdbKSgrRZGezQ?Z-b^gVPYo4n7sTiXs1Qda5WdHf0I*)N` z9}hZWqaME)GdviLAENS2tBt6C2lK1vg>n3=z~AA+=J4WHvpex!ybHqTrzIsR2uwBE z^sGLJGAuTS2%U#_Q6klPdu#v8E@!i{iv=vByw84H7U!#DPMrx_6UFIbXf`?lm}n#5 z&|tVzgpKvLtEWi}uDH&8yuJDqowD{B+Yv>uzIp>cn+jx^Js? zF``PDwlSy&{)y1iOnEmFHHpWB)a!Pc|MTlwMu7Q-bMFR$_LTNhM{w@r_TEJlWO zfudwq*h?x%gsN1SzY~)EL-9bjUlV9rSl;d3xtWi7kKm=ln<^3T6D;$?6e^#Q>7of- zt0w1i*!~ibMVda>ssV;s^t>Oy(ygr`i?5xhJiU4F7uZK3$qTMgI%IlY1JgVy>sSg5xPrCRdxYcHX*qg5G;8F-Y0CgzZVybS=Xb0{hp zEys2_6Q60XhpBYuc--S<1617BIC>0zw7QPmTe19E_e`AP`PzT$gv;~QgtfndRcCj+ zB!NtS?uKmUmp24HzY;ELtTe-!KirXS83K`vl)v&ea`_e#7l>mA}=t z&Kk--Lk|#{zMa9x-{Snz^^C~LpZp#(Vp|4?BIJ&f41++jOin(ekZl5FIib{NGwNd=)0o{b9myEK|x zf5pad_CHZyyfGVres#QVI4y3R*gXiJ5f6l;Wyjs#4Aw`}MYR5=#O0yX9nk*vdegtw zlYr${O{zf8JA~r+9*Zz9+7i1=y%@r~eP=DJ7SO?6iiq7M0mvZi zskGS}#0S`yl1*o5wNfxW*6<|5xBbbcXH`;>=TPfYPa-Ql^G}d$>KFnbYJkA5Z#Z>s z?ef9B<@~-b)XHTfGs{ZQ=8_aX$oO(J&A|3v|!uj^>?o)L(Tk+kt!0AmsYpj&*`hRZ={If+c~&gUe7fn|XX z^s|HTZ>%RnNoPodPIxvi?sv5p7%JgZK+qB<y}>cn%V6i#pWe%#?M4h6U4tgfFuVXSG zem|&g`u~9e5|y7CHP%T$lkECSL>d*Q{W!(rNT^ zrM!;efisInT|Stx4lB*mdRye;o5O(_JF&pJkiNGin!o6l`AP@ZY#Rl+i9h4Jb;?W# z+|7`MTV>73ZKG_TL_PyBt^QH>q%(?8olyb4&h(=IW1>QsK!mwdj-6*&`)8}1p(^6(+SDz;{;*5|M z@U*rZDmjUy!*>r^x?aUJlG-I`IWvhp6?yI&)+ERsFN;5!&lBO9|8XeGeGigq2P+Q6 z+bUvoimA`{-K%*WsbHTeYv^`d2;6F-PX*^6jHUvZa*sRV z22N|{zI`!S%_Xks#IPL}>$U#{6b^+YaMjfx70ol}!Lh>6mj-Uu4f1`3TWr9fAh!925Ej;HhpTbupyVH zO^at_gey_Ql-&U;U&sB!6^O4^aZSEQ&Ns}MjqeeX>F~U5f#0WV6P0e5 z@r0}=LOD3g7f}wc@E4Vq>mbXTw6GQsMC5w6)BPowY>SF;fMknX?Y==szgmkvqmwfl z1^7uI_sCSW)>r^-*Yv&m*lK6No5~BSOqTt-P<3mGiv0Pb)XeE!j(u=5!Hqki(N6-d zy$*argrJb<9X&1&N~w{JSp7oRxET9`pW${TM8z3ARK_mh{|h}#WAP0P_&!;@p^3}k z_j~LlL=DBnt_p28m6ujWnqsR5tDAmGlxbau7vzbonG1l`E!r4!Pfc6LXkf7L48^+$ zO)^9_j{>VbKt*5RI}zCbi5jBkBfoh*+c33DwQDz*=}hHpcRQgR zi}-!DS8%Brcj{^ApKUZwgoJ;;7|2=o=a?LP^?O+esw)mIx+eq-Of zH->`*Qd0(5xC>k6$TsMLYdh6Kj{{U*M_SQ?leUPuyN{pj9E6`LPICN%>K(&D1p|A7 zmf?fK)ky#>(p=%JPGm|KDj}T(s_mT-0*2=b2)Gmc% z0bOlwAnYEX4~G6@&JF`?T>Ulf0#jQ963sWIL2Yms#Er_>=99x-fxWKdc{p(f$D~Rm zS1T3*9|(b8c=HnyfkhV$uYUioseDTg>}NUb!~4?d#_PJv-X^M&fD7*jgy3VZ!yi1D zk|jc@A;6w^T3R^wo9zrRriM7H%RVEt0IBc0$nW!AfQkzaYvQ`5a|G(Lv<*1n#2vVJ zN5>aS%tW6c76dm>A=mNaedmsyQ1RqXqKIFa#IWiJUKk8>Xt>wYl#t;mdd~wALk#@+ z0xIdfebk|j-7;^_3J?NR;E7HIz?&Zop<1-S78?kkK~h_Xb)6F-o|gZy(8UD>D=M#~ zgVN!yUJ*c?@!+zhDPT26_J}Z5mWf-}@t80u+Q`cRpyLLnLFHI-a~C4fHwablf=-2<=|tA7 zw&ajq&FU2DqIfbCdvfW2IBXjh`WQ3iE;tRG3FN`ieJV@P>>w%JM!JWN(^R7d{v3Z9 zWZ5`O6tHs{0(E5nPoRT_)95Ar@(bLG?YuX7vP9v~Y6`oh*g!aXBmC^iHBC`a=!Ne- znMvyFjhDCfqU4JT46%ax@DpsMdKK{7f}oNgH#)(cj$sF4j`xtX38-49w1D-m!E$2! zMY#`4jImqwGMX?$Z`|@&2w(xI#>Czx93%RNm)))JU}I+1eRRFxZmolM(qJoj@4Lw? z&pS`H*v!DWb#VEIzeFQZhyLc%6UBlYVR^5oiDC|e11TWG#9%4U&>p-{|J|b%_-ie2 ztC5zbD+2eE6Sy8IT?6z#x8ucl-FdSMJ3)Hl9ORN9z0H+&e2OGQiw}3Tg>-*2;D*x4 z?HVhf{)nr6I+p$q|0@}h_v;)?%7NRrV3is#HA|$sM@nYfIz)gVK@sL8gze!!7^&U7 zM;Al5Z|k2+=_ds)n+a8h`V8Wf6ogwq{^D}QqThr~8(lGp;3q7P& z=D`*IC~GlFJT2Zo{UC~naE5-k1d3SN@hZYMVgfUeUa!DVZ~I+lz%El4;BUuANj zq>#ffkSnme1je-=)5BEA}iTDOIPse{Je<29=ctvo-bBsn%oq^B>qx$Jyztc~1`WFVm%(7g#5=m2p6^4Awdu>6n59k08ma!KYJ0~QOcbB*wF!y`rra1L=q3B0jS0J-(Whv`>!S7q zMV(^&z%c6AM`pz7PITJOZF~o#Khs-RJjW~Mq6}H@qQNrJasb_;NoVefQE&C>` z>9kX-ov0!6V$`smuzl1f<7j1VVK+&t6YF@jAh;9~3W(TN>gf48p9z6A6g+8Por!P6 zjys|9RL zXT@W%q@cS!h=v%8%?~=*VBKL5tKC=SmLew{9_|__dSGvI0j+aBhVJ*2lg&LQ`Gn7Z zd^9Az6!9GQOK}HIuhe*Pib?!dUjDdvsh-J6!i4Z zI7W;}^^UW8hwQckk*r6%&EEMFV;lEgh?}JSuBRvYkwe01LJKJJn#=*>jdGr7G16xE z%eoFc);%iBk&=x`rRF6_1DgDVs<;E*ifgf@*j=00ID=Bb81?fEKTo}gX?LfBktFlC z$K+5644yR${aJWqFqJ~J?s(jQKbcv4B6S+k_<)y5Pxqm14EZsL#xWV+qCa>DldejjWrM?U`x`D)xX8R%6<3OoiLTYaR6cY4nc=)e+f! zpCSDHQSejU5{2_5q(H!0`AehKvI5g!JgvXy^RMuUM^z^uGH{;w3IW$3_^XgJ+X76EazXqK0UIRqZyp}JnEsVx7!Q8wJGnv;;#M^8MFV*LwaUYeoP;r!n8;KE;y_cq^6 z)vATwd<3mqT}_SFCp;}y;5xoO^;WSe(ZVyKTVhd>P1>wlX6}sMlHDFFD?NR`( zOmx%WB$rE29tkHQ6^-{s zYTEt^@7N(hOv4nwYqqCwbbj2G06%`{`7`H5Y?IU10slWYhw1;}XIzl-)cB!H`I*+! z7PL)1oH=x$-gqzRGZxBql|Xlorr^IAa3e(+I(?nLNek({G2r~=_TkFD<6sloQs%RB zO=-qo*q*T8qw1qGLjiUUmhiFf8c|_JxUr9wm!)B&#Pu21M-alhdOUo~C!g7z$!_^= z-#e_WYY|b{ss1~;!O>lYs<}`bMKyjz>Xe>oz2&FA^xGKB@)QdE2`{!>s`FKod)Kq% zIQSwA(mOx&%FJMOIO5=eQ*+9=vIl{+^vwkl;vM#g(Z#w!&DOpA9wWxqx60|tgzdK_ zA-A3pimQCrdm8i@G6L%axRoaYX(gWSO4+U9=|Y6ISR;LVgG%cC~+RkdH=Xd~H3sWW&1PJKS-1TB;T&ka^<1hg%Qh zmGV7%mX?0(Zc@1d!}M&$a~)s3cFLtNZyfIm^L;^i%p@M^BP|~li>8GC>%P3doS)*= zKR9}a=g-mo)GBxCgU4gB8Se+>-K$+P|8%<;jY1^MoDh6!dft1XCK4*&bzKd;vtN6> z8;_)ac~a{eD{C{H5#Fv9B>WjW^9_#wsY3EwETCpah%s`m`g@64zeeUO1L#ZX+sx zaAUHv${-0*nl_R_uN^sqS5G|(BdCUA*4^d^f|yr~qVFfmyCRL$_z-Dxnna-Qc+1)Vn1tM%I~9B7Og!nvF@c!Dk&GATOFPcmL#h zJiVdztRX^pEtUj85DL#9Sux=z#`fT-S*L~>)zyO%?Ie$??Vi78%uB^}ib&1CtcLViBS~YU_bWTKGiacXCVl)3d zV8U?d1CHJ5jUU&LisU_yQ&;(Qy5eK+;H%1{sUs8-+j8A05Iu-Mhf9% zdDV7}?9bq8pCGzga&)jDXjsPwGv!N2k2?UgDTLuILm3D7`syF4 zn_c?68mdrt18j}w`mC-2fY>}%qbK?KaUX&x%P9|xG5H7bq_7sdyThK3;P{?)qtki68m3>Sw}Jw{Viq9L zasZ(3j&EJYEqw@!(sm``92&!m*Z`YS2lewcbHrO%?I_aq+6QrL#gaXseiV$CT^c<& z3QT#V$I^VU@5fkP;KFKRG5^R>7x|U%_{407bZ+1~zitlj5O($Yh1Wtf9X@mpUa*un zg)KHLfaucQgcyD6g2$?tg$e}{x^)n0h=5SiVZ|N3*#Ljqk=Tc=ONSIhk+zDuNGz~RS$dmHj;$$o{@#*5y(wYei0$Ox*J}8kmP+a z_<~2#9i1Mqta`1RD9hs|N;cB@vM{p8s(~V%4giLYH^rX7r(15bnw)mZhYuOVQ*?uS zH$98i)f@d=O>W%wqo|H4XCbH*V|omT1L#oU0+YeRLzEU)T!ex4fr#(Y#FoUttD1Am z8`Nsz*>?y)n(9sP^53`r?fur7ZK)qp`g|yNm_^?f{=8y)r2VR*UuI*rwK#AwedU#} z--|boVxALf7eJ5TCIIZ^ z5$-|ucDAA@`%Jtc*5kK-qh^e%jhFFrx0K}SJztUA)7^qd`nX2zhT1636|%M zPn6oofNYk*)6zuDi=8}dAKyRO>u_#>G(v;;=roHlv-)hes#IrI>DzR!2MzvA0(7&l=6n#hwU-xw99}S*QF@ld6VMb`aNnor zv*((Kfrw9k7B{i0kMzDb??R}bAG*5l?2pG`d1T>g0swG^FQU)I6)~i{>DS>hScd%` zfIW`1ym3H{Ux( zri-NFlm`ouf7!`^;ra?FI1|YMrnmZ5;x~CY*-dwnRudgAbfU!SaOJ5c}y;N_n!-X1QONKdX$as+;bYR05#De$WIgH(9bfgM42) zmGk{SO1buMD7QWQA#w&!Nup74>~X(LiY>`-v`gdOmirhgw_I}_#Td;=xkZMM?b0SG zg*M}UoAEGX!gfo-V3OM)!w?g5Ip6H({Bi!?=lf%Q&sxv3*7{?u=eO2+-}m>j?qS20 z1#8{{#JqzHDY_n|s?k4FfQ%nC3C`DcYMzlVJ*z7TFs_04s(Z@iiB;F?U5#~cW#%U~ zz#!LKd61W(jN2$oH}i5U&MdXn&bF2VO+3z4)blDBLpnR02tP)q%$}Zm8b>DxfPOc; zxP>vPtfUtu!P;SeJWJcP?zE#yhytWs=ly;%g2LLoZ9Lng&evBH?!!Rca~c-3BBr?1 z8NH0bsa+@Yl@?gddPWr!7&MXqF6=zPz3jlVL;->ymdf%~Pp@*|-0nLfp<<(MQBg1Y ziGHg+vq@v?b>s+OXAK5<$6Netg9;TSMQ2JK_x?+#RCsMoLx;gvth`pz0HX(2X1B@3 z^lYt6H)v@Nv3ADL&fv-B$aFx_%{Bj&+k)2yncfe}mlMpUJ~=V(aLB?hI*3tMs%VU7 zzpQ5=bPQ+{mvBq&$mZ!7$v|5LfW#+y$MVo4O~}dQy)NA-*G|=b|8piEAd0}aH1!{C zLk=k7l8Y9)#r8}f^H@rcWT1z*2!JNWTVJ8+JcmKyHhqF4#&jIHD%z<5MNCWRQWbwu zxC|p3qSQi~S==V8FMY}>oUGv5!MA2Bmj=Ly8`@<$VD)IfQAsIlhQO(sPQ~4Sk(Hy> zQz~h(eFjGC#=_1SzgW1kE1`8X;c>bI>}QCY4kHRoKlPhFAf~70Wjvu!4Mbs3_>xv(K*W+(hu#jaY^ryw9BTQ-r|2@wRJ~Nn09AC=p=6BN8@Nn>)Y` zhxh&H(9XOXmggmip9y)k`qG&SiRFQ`LA`AE_+ig7kr{L@O09{}Phk;EEH6iZK=5I} zqQr)q8>>`8lDdBbHQT9gu%GTwZQ8}^RZ=4X3x?458pN3SvW8{w@u@=zHy@hGgh8JK zV z>*!fou%QHd;z5Eq0U{tJ8SO*Yn89jva|ZvRPTod&#}TpNx)H!iPxUT+QlNU3cnm4#*~vME&h^?w(^0pNMbH0zUQwb&c~p!}&< zACF+i~Q5PJ`-MAKQLaeN_Yt)*v zPuPxms4NCNzH`VPv0P%3nuf@R?dDL9FEvL=R6UkKO8uTLxf1Ld1(GfnIZ5mlB`iYxOKM;N{pq{%f6N5jBM(?W z#CsyL+HQI{pLZl;_*-bAPyPI_TzqvG|^9N?+cgIf5H zvgJ3!u1y@~#w_-h?Z`K!(U^ax#%6t(gBnbsl|9WA;}+MfC`*|k8|Ii}>vXtKfOboF1+$Xdx%C=Oo^2tkrqy zp5k>RFkD27pX_Z*Y)6uLt-}hzpH#9~e zo#M|qD*K?cW8Lf-CTh}Q08hib#i>I2N_7tGCH=!gAGQZT8THrKp+*(drB-mwU^6MdkHuO=%` zhxUO$R194a@W=`RCAvN?juYWkJ&!p$PX#QTp!j4Zd&A=5%dy_KjTLrkzP-z6NS$Kc zNN)Vubd2%LG3m(fK#+n|y>_ zg=EWrM-{SrfRC{FpGMMO_5XLOzu!NJ$q String { + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn constant_time_eq(a: &str, b: &str) -> bool { + let (a, b) = (a.as_bytes(), b.as_bytes()); + a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 +} + +pub fn authorize_url(endpoint: &Url, state: &str) -> Url { + let mut url = endpoint.clone(); + url.query_pairs_mut() + .append_pair("app_name", APP_NAME) + .append_pair("app_id", APP_ID) + .append_pair( + "success_url", + &format!("{CALLBACK_SCHEME}://auth/callback?state={state}"), + ) + .append_pair( + "reject_url", + &format!("{CALLBACK_SCHEME}://auth/rejected?state={state}"), + ); + url +} + +/// Opens the authorization window. Completion is reported to the shell via +/// the `auth://completed` or `auth://failed` events. +pub fn begin( + app: &AppHandle, + discovery: SiteDiscovery, + allow_insecure_local: bool, +) -> AppResult<()> { + let endpoint = discovery + .authorization_endpoint + .clone() + .ok_or(AppError::ApplicationPasswordsUnavailable)?; + let state = random_state(); + let url = authorize_url(&endpoint, &state); + *app.state::() + .pending_auth + .lock() + .expect("poisoned") = Some(PendingAuth { + state, + discovery: discovery.clone(), + allow_insecure_local, + }); + + if let Some(existing) = app.get_webview_window(AUTH_WINDOW) { + existing.close().ok(); + } + + let handle = app.clone(); + WebviewWindowBuilder::new(app, AUTH_WINDOW, WebviewUrl::External(url)) + .title(format!("Sign in to {}", discovery.name)) + .inner_size(520.0, 720.0) + .resizable(true) + .center() + // Fresh, throwaway cookie jar: nothing from this sign-in persists. + .incognito(true) + .on_navigation(move |nav| { + if nav.scheme() == CALLBACK_SCHEME { + let handle = handle.clone(); + let nav = nav.clone(); + tauri::async_runtime::spawn(async move { + complete(&handle, nav).await; + }); + return false; + } + // SSO providers may live on other hosts, but never allow + // downgrades or non-web schemes in the sign-in window. + nav.scheme() == "https" + || (allow_insecure_local && nav.scheme() == "http") + || nav.scheme() == "about" + }) + .build()?; + Ok(()) +} + +#[derive(Deserialize)] +struct Introspection { + uuid: String, +} + +async fn complete(app: &AppHandle, callback: Url) { + let result = handle_callback(app, &callback).await; + if let Some(w) = app.get_webview_window(AUTH_WINDOW) { + w.close().ok(); + } + match result { + Ok(summary) => { + app.emit_to("main", "auth://completed", summary).ok(); + } + Err(e) => { + app.emit_to("main", "auth://failed", e.to_string()).ok(); + } + } +} + +async fn handle_callback(app: &AppHandle, callback: &Url) -> AppResult { + let pending = app + .state::() + .pending_auth + .lock() + .expect("poisoned") + .take() + .ok_or(AppError::AuthFlow)?; + + let mut state = None; + let mut user_login = None; + let mut password = Zeroizing::new(String::new()); + for (k, v) in callback.query_pairs() { + match k.as_ref() { + "state" => state = Some(v.into_owned()), + "user_login" => user_login = Some(v.into_owned()), + "password" => password = Zeroizing::new(v.into_owned()), + _ => {} + } + } + if !state.is_some_and(|s| constant_time_eq(&s, &pending.state)) { + return Err(AppError::AuthFlow); + } + if callback.host_str() != Some("auth") || callback.path() != "/callback" { + return Err(AppError::AuthFlow); + } + let user_login = user_login.ok_or(AppError::AuthFlow)?; + if password.is_empty() { + return Err(AppError::AuthFlow); + } + + let creds = Credentials::new(&user_login, &password); + let record = finalize_site(app, pending.discovery, creds, pending.allow_insecure_local).await?; + Ok(record.summary()) +} + +/// Verifies credentials against the site, then stores the password in the +/// keychain and the site metadata on disk. +pub async fn finalize_site( + app: &AppHandle, + discovery: SiteDiscovery, + creds: Credentials, + allow_insecure_local: bool, +) -> AppResult { + let client = WpClient::new(discovery.api_root.clone(), &creds, allow_insecure_local)?; + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await?; + let app_password_uuid = client + .get::("wp/v2/users/me/application-passwords/introspect", &[]) + .await + .ok() + .map(|(i, _)| i.uuid); + + let avatar_url = ["96", "48", "24"] + .iter() + .find_map(|k| me.avatar_urls.get(*k)) + .and_then(|u| Url::parse(u).ok()) + .filter(|u| u.scheme() == "https"); + + let store = &app.state::().store; + let record = store.upsert(SiteRecord { + id: Uuid::new_v4(), + name: discovery.name, + url: discovery.site_url, + admin_url: discovery.admin_url, + api_root: discovery.api_root, + username: creds.username.clone(), + display_name: if me.name.is_empty() { + creds.username.clone() + } else { + me.name + }, + avatar_url, + app_password_uuid, + bridge_available: discovery.bridge_available, + allow_insecure_local, + added_at: chrono::Utc::now(), + })?; + if let Err(e) = keychain::save(&record.id, &creds) { + store.remove(&record.id).ok(); + return Err(e); + } + Ok(record) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builds_authorize_url_with_state() { + let ep = Url::parse("https://example.com/wp-admin/authorize-application.php").unwrap(); + let u = authorize_url(&ep, "abc"); + let pairs: std::collections::HashMap<_, _> = u.query_pairs().into_owned().collect(); + assert_eq!(pairs["app_name"], APP_NAME); + assert_eq!(pairs["app_id"], APP_ID); + assert_eq!( + pairs["success_url"], + "wpremoteeditor://auth/callback?state=abc" + ); + assert_eq!( + pairs["reject_url"], + "wpremoteeditor://auth/rejected?state=abc" + ); + } + + #[test] + fn state_is_random_and_long() { + let (a, b) = (random_state(), random_state()); + assert_eq!(a.len(), 64); + assert_ne!(a, b); + } + + #[test] + fn constant_time_eq_works() { + assert!(constant_time_eq("abc", "abc")); + assert!(!constant_time_eq("abc", "abd")); + assert!(!constant_time_eq("abc", "abcd")); + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..622d45d --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,256 @@ +//! IPC commands for the shell webview. Each must also be listed in build.rs +//! and granted in a capability file. + +use crate::auth; +use crate::editor::{self, Bounds, EditorCommand, EditorReport}; +use crate::error::{AppError, AppResult}; +use crate::keychain::{self, Credentials}; +use crate::wp::client::{header_u64, WpClient}; +use crate::wp::discovery; +use crate::wp::models::*; +use crate::AppState; +use std::collections::HashMap; +use tauri::{AppHandle, Manager, State, Webview}; +use uuid::Uuid; + +fn client_for(state: &AppState, site_id: &Uuid) -> AppResult<(crate::sites::SiteRecord, WpClient)> { + let site = state.store.get(site_id)?; + let creds = keychain::load(site_id)?; + let client = WpClient::new(site.api_root.clone(), &creds, site.allow_insecure_local)?; + Ok((site, client)) +} + +fn valid_rest_base(s: &str) -> AppResult<&str> { + if !s.is_empty() + && s.len() <= 64 + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + Ok(s) + } else { + Err(AppError::Internal("invalid content type".into())) + } +} + +#[tauri::command] +pub async fn discover_site(url: String, allow_insecure_local: bool) -> AppResult { + discovery::discover(&url, allow_insecure_local).await +} + +#[tauri::command] +pub async fn begin_authorization( + app: AppHandle, + url: String, + allow_insecure_local: bool, +) -> AppResult<()> { + let d = discovery::discover(&url, allow_insecure_local).await?; + auth::begin(&app, d, allow_insecure_local) +} + +#[tauri::command] +pub async fn add_site_manual( + app: AppHandle, + url: String, + username: String, + app_password: String, + allow_insecure_local: bool, +) -> AppResult { + let app_password = zeroize::Zeroizing::new(app_password); + if username.trim().is_empty() || app_password.trim().is_empty() { + return Err(AppError::Unauthorized); + } + let d = discovery::discover(&url, allow_insecure_local).await?; + let creds = Credentials::new(&username, &app_password); + Ok(auth::finalize_site(&app, d, creds, allow_insecure_local) + .await? + .summary()) +} + +#[tauri::command] +pub fn list_sites(state: State<'_, AppState>) -> Vec { + state.store.list().iter().map(|s| s.summary()).collect() +} + +#[tauri::command] +pub async fn remove_site(app: AppHandle, site_id: Uuid) -> AppResult<()> { + let state = app.state::(); + let site = state.store.get(&site_id)?; + // Best effort: revoke the Application Password on the server. + if let (Some(uuid), Ok((_, client))) = (&site.app_password_uuid, client_for(&state, &site_id)) { + if uuid.chars().all(|c| c.is_ascii_hexdigit() || c == '-') { + if let Err(e) = client + .delete(&format!("wp/v2/users/me/application-passwords/{uuid}"), &[]) + .await + { + log::warn!("couldn't revoke application password: {e}"); + } + } + } + if state + .editor + .lock() + .expect("poisoned") + .as_ref() + .map(|s| s.site_id) + == Some(site_id) + { + editor::close(&app)?; + } + keychain::delete(&site_id)?; + state.store.remove(&site_id)?; + editor::clear_site_data(&app, &site_id); + Ok(()) +} + +#[tauri::command] +pub async fn get_site_context(state: State<'_, AppState>, site_id: Uuid) -> AppResult { + let (site, client) = client_for(&state, &site_id)?; + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await?; + let (types, _) = client + .get::>("wp/v2/types", &[("context", "edit".into())]) + .await?; + let mut post_types: Vec = types + .values() + .filter(|t| t.viewable && t.slug != "attachment") + .filter(|t| t.rest_namespace.as_deref().unwrap_or("wp/v2") == "wp/v2") + .map(|t| post_type_access(t, &me.capabilities)) + .filter(|a| a.can_create || me.capabilities.get("edit_posts").copied().unwrap_or(false)) + .collect(); + // Posts, then Pages, then everything else alphabetically. + post_types.sort_by_key(|t| match t.slug.as_str() { + "post" => (0, String::new()), + "page" => (1, String::new()), + _ => (2, t.name.to_lowercase()), + }); + Ok(SiteContext { + site: site.summary(), + user_id: me.id, + roles: me.roles, + post_types, + }) +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListPostsQuery { + site_id: Uuid, + rest_base: String, + status: Option, + search: Option, + page: Option, + mine_only: bool, + can_edit_others: bool, + user_id: u64, +} + +#[tauri::command] +pub async fn list_posts(state: State<'_, AppState>, query: ListPostsQuery) -> AppResult { + let ListPostsQuery { + site_id, + rest_base, + status, + search, + page, + mine_only, + can_edit_others, + user_id, + } = query; + let (_, client) = client_for(&state, &site_id)?; + let rest_base = valid_rest_base(&rest_base)?; + let page = page.unwrap_or(1).clamp(1, 10_000); + let status = match status.as_deref() { + Some(s @ ("publish" | "draft" | "pending" | "future" | "private" | "trash")) => { + s.to_string() + } + _ => "any".to_string(), + }; + let mut q: Vec<(&str, String)> = vec![ + ("context", "edit".into()), + ("per_page", "30".into()), + ("page", page.to_string()), + ("status", status), + ("orderby", "modified".into()), + ("order", "desc".into()), + ("_fields", "id,title,status,modified,author,link".into()), + ]; + if let Some(s) = search.filter(|s| !s.trim().is_empty()) { + q.push(("search", s.chars().take(200).collect())); + } + if mine_only { + q.push(("author", user_id.to_string())); + } + let (posts, headers) = client + .get::>(&format!("wp/v2/{rest_base}"), &q) + .await?; + let items = posts + .into_iter() + .map(|p| PostSummary { + can_edit: can_edit_others || p.author == Some(user_id), + title: p + .title + .and_then(|t| t.raw.or(t.rendered)) + .unwrap_or_default(), + id: p.id, + status: p.status, + modified: p.modified, + author: p.author, + link: p + .link + .filter(|l| l.starts_with("https://") || l.starts_with("http://")), + }) + .collect(); + Ok(PostPage { + items, + total: header_u64(&headers, "x-wp-total"), + total_pages: header_u64(&headers, "x-wp-totalpages"), + page, + }) +} + +#[tauri::command] +pub async fn trash_post( + state: State<'_, AppState>, + site_id: Uuid, + rest_base: String, + post_id: u64, +) -> AppResult<()> { + let (_, client) = client_for(&state, &site_id)?; + let rest_base = valid_rest_base(&rest_base)?; + // Without `force`, WordPress moves the post to the trash (recoverable). + client + .delete(&format!("wp/v2/{rest_base}/{post_id}"), &[]) + .await +} + +#[tauri::command] +pub async fn open_editor( + app: AppHandle, + site_id: Uuid, + post_type: String, + post_id: Option, + bounds: Bounds, +) -> AppResult<()> { + editor::open(&app, site_id, &post_type, post_id, bounds).await +} + +#[tauri::command] +pub fn close_editor(app: AppHandle) -> AppResult<()> { + editor::close(&app) +} + +#[tauri::command] +pub fn editor_set_bounds(app: AppHandle, bounds: Bounds) -> AppResult<()> { + editor::set_bounds(&app, bounds) +} + +#[tauri::command] +pub fn editor_command(app: AppHandle, command: EditorCommand) -> AppResult<()> { + editor::command(&app, command) +} + +#[tauri::command] +pub fn editor_report(app: AppHandle, webview: Webview, report: EditorReport) -> AppResult<()> { + editor::report(&app, &webview, report) +} diff --git a/src-tauri/src/editor.rs b/src-tauri/src/editor.rs new file mode 100644 index 0000000..e265d44 --- /dev/null +++ b/src-tauri/src/editor.rs @@ -0,0 +1,410 @@ +//! The editor pane: a child webview that loads the site's own block editor, +//! so every block, sidebar plugin and meta box works exactly as in wp-admin. +//! +//! The webview is treated as untrusted remote content: +//! - navigation is locked to the site's origins; other links open in the +//! system browser; +//! - each site gets its own cookie store; +//! - it may call exactly one app command, `editor_report`, granted at runtime +//! for that site's origin only; +//! - the core controls it by evaluating fixed scripts, never page-supplied code. + +use crate::error::{AppError, AppResult}; +use crate::keychain; +use crate::security::same_origin; +use crate::sites::SiteRecord; +use crate::wp::client::WpClient; +use crate::AppState; +use serde::{Deserialize, Serialize}; +use tauri::ipc::CapabilityBuilder; +use tauri::webview::WebviewBuilder; +use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, Webview, WebviewUrl}; +use tauri_plugin_opener::OpenerExt; +use url::Url; +use uuid::Uuid; + +pub const EDITOR_LABEL: &str = "editor"; +const BRIDGE_JS: &str = include_str!("editor_bridge.js"); + +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct Bounds { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl Bounds { + fn sane(self) -> Self { + let clamp = |v: f64| { + if v.is_finite() { + v.clamp(0.0, 20_000.0) + } else { + 0.0 + } + }; + Self { + x: clamp(self.x), + y: clamp(self.y), + width: clamp(self.width).max(1.0), + height: clamp(self.height).max(1.0), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum EditorCommand { + Save, + Publish, + Undo, + Redo, + ToggleSettings, + DeviceDesktop, + DeviceTablet, + DeviceMobile, +} + +impl EditorCommand { + fn script(self) -> &'static str { + match self { + Self::Save => "window.__wpre && window.__wpre.save()", + Self::Publish => "window.__wpre && window.__wpre.publish()", + Self::Undo => "window.__wpre && window.__wpre.undo()", + Self::Redo => "window.__wpre && window.__wpre.redo()", + Self::ToggleSettings => "window.__wpre && window.__wpre.toggleSettings()", + Self::DeviceDesktop => "window.__wpre && window.__wpre.device('Desktop')", + Self::DeviceTablet => "window.__wpre && window.__wpre.device('Tablet')", + Self::DeviceMobile => "window.__wpre && window.__wpre.device('Mobile')", + } + } +} + +/// State reported by the bridge script. Every field is bounded; anything +/// unexpected is rejected by serde. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EditorReport { + pub kind: String, + #[serde(default)] + pub post_id: Option, + #[serde(default)] + pub post_type: Option, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub status: Option, + #[serde(default)] + pub link: Option, + #[serde(default)] + pub dirty: Option, + #[serde(default)] + pub saving: Option, + #[serde(default)] + pub can_publish: Option, + #[serde(default)] + pub path: Option, +} + +impl EditorReport { + pub fn validate(mut self) -> AppResult { + if !matches!(self.kind.as_str(), "editor" | "login" | "page") { + return Err(AppError::Internal("invalid editor report".into())); + } + let cap = |s: &mut Option, n: usize| { + if let Some(v) = s { + if v.chars().count() > n { + *v = v.chars().take(n).collect(); + } + } + }; + cap(&mut self.post_type, 64); + cap(&mut self.title, 500); + cap(&mut self.status, 32); + cap(&mut self.path, 512); + // Only pass through web links; the shell may offer "View on site". + if let Some(l) = &self.link { + if l.len() > 2048 || !(l.starts_with("https://") || l.starts_with("http://")) { + self.link = None; + } + } + Ok(self) + } +} + +pub struct EditorSession { + pub site_id: Uuid, +} + +fn origin_pattern(u: &Url) -> String { + format!("{}/*", u.origin().ascii_serialization()) +} + +fn editor_target(site: &SiteRecord, post_type: &str, post_id: Option) -> AppResult { + if !post_type + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + || post_type.len() > 20 + { + return Err(AppError::Internal("invalid post type".into())); + } + let mut url = match post_id { + Some(id) => { + let mut u = site + .admin_url + .join("post.php") + .map_err(|_| AppError::InvalidUrl)?; + u.query_pairs_mut() + .append_pair("post", &id.to_string()) + .append_pair("action", "edit"); + u + } + None => { + let mut u = site + .admin_url + .join("post-new.php") + .map_err(|_| AppError::InvalidUrl)?; + if post_type != "post" { + u.query_pairs_mut().append_pair("post_type", post_type); + } + u + } + }; + url.set_fragment(None); + Ok(url) +} + +#[derive(Deserialize)] +struct Handoff { + url: String, +} + +/// With the companion plugin active, trade the Application Password for a +/// single-use, short-lived login link so the user isn't asked to sign in again. +async fn handoff_url(site: &SiteRecord, target: &Url) -> AppResult { + let creds = keychain::load(&site.id)?; + let client = WpClient::new(site.api_root.clone(), &creds, site.allow_insecure_local)?; + let h: Handoff = client + .post( + "wpre/v1/handoff", + &serde_json::json!({ "redirect_to": target.as_str() }), + ) + .await?; + let url = Url::parse(&h.url).map_err(|_| AppError::BadResponse)?; + if !same_origin(&site.admin_url, &url) { + return Err(AppError::BadResponse); + } + Ok(url) +} + +pub async fn open( + app: &AppHandle, + site_id: Uuid, + post_type: &str, + post_id: Option, + bounds: Bounds, +) -> AppResult<()> { + let state = app.state::(); + let site = state.store.get(&site_id)?; + let target = editor_target(&site, post_type, post_id)?; + let initial = if site.bridge_available { + handoff_url(&site, &target).await.unwrap_or_else(|e| { + log::warn!("editor handoff unavailable, falling back to site login: {e}"); + target.clone() + }) + } else { + target + }; + + // Reuse the existing pane for the same site; recreate it for another site + // so cookie stores never mix. + let same_site = state + .editor + .lock() + .expect("poisoned") + .as_ref() + .map(|s| s.site_id) + == Some(site_id); + if let Some(existing) = app.get_webview(EDITOR_LABEL) { + if same_site { + existing.navigate(initial)?; + set_bounds(app, bounds)?; + return Ok(()); + } + existing.close()?; + } + + let allowed: Vec = vec![site.admin_url.clone(), site.url.clone()]; + let origins: Vec = allowed + .iter() + .map(|u| u.origin().ascii_serialization()) + .collect(); + let config = serde_json::json!({ "origins": origins }).to_string(); + let script = BRIDGE_JS.replace("__WPRE_CONFIG__", &config); + + // Grant the remote page exactly one command, only on this site's origins. + let mut cap = CapabilityBuilder::new(format!("editor-remote-{site_id}")) + .webview(EDITOR_LABEL) + .permission("allow-editor-report"); + for u in &allowed { + cap = cap.remote(origin_pattern(u)); + } + app.add_capability(cap)?; + + let handle = app.clone(); + let nav_allowed = allowed.clone(); + let mut builder = WebviewBuilder::new(EDITOR_LABEL, WebviewUrl::External(initial)) + .initialization_script(&script) + .on_navigation(move |url| { + if matches!(url.scheme(), "about" | "blob" | "data") { + return true; + } + if nav_allowed.iter().any(|a| same_origin(a, url)) { + return true; + } + if matches!(url.scheme(), "https" | "http" | "mailto") { + handle.opener().open_url(url.as_str(), None::<&str>).ok(); + } + false + }); + + #[cfg(target_os = "macos")] + { + builder = builder.data_store_identifier(*site_id.as_bytes()); + } + #[cfg(not(target_os = "macos"))] + { + let dir = app + .path() + .app_local_data_dir()? + .join("editor-sessions") + .join(site_id.to_string()); + builder = builder.data_directory(dir); + } + + let window = app + .get_window("main") + .ok_or_else(|| AppError::Internal("main window missing".into()))?; + let b = bounds.sane(); + window.add_child( + builder, + LogicalPosition::new(b.x, b.y), + LogicalSize::new(b.width, b.height), + )?; + *state.editor.lock().expect("poisoned") = Some(EditorSession { site_id }); + Ok(()) +} + +pub fn set_bounds(app: &AppHandle, bounds: Bounds) -> AppResult<()> { + if let Some(w) = app.get_webview(EDITOR_LABEL) { + let b = bounds.sane(); + w.set_position(LogicalPosition::new(b.x, b.y))?; + w.set_size(LogicalSize::new(b.width, b.height))?; + } + Ok(()) +} + +pub fn command(app: &AppHandle, cmd: EditorCommand) -> AppResult<()> { + let w = app + .get_webview(EDITOR_LABEL) + .ok_or_else(|| AppError::Internal("no editor open".into()))?; + w.eval(cmd.script())?; + Ok(()) +} + +pub fn close(app: &AppHandle) -> AppResult<()> { + if let Some(w) = app.get_webview(EDITOR_LABEL) { + w.close()?; + } + *app.state::().editor.lock().expect("poisoned") = None; + Ok(()) +} + +/// Called from the remote page. Rejects calls from any webview but the editor. +pub fn report(app: &AppHandle, webview: &Webview, report: EditorReport) -> AppResult<()> { + if webview.label() != EDITOR_LABEL { + return Err(AppError::Forbidden); + } + let report = report.validate()?; + app.emit_to("main", "editor://state", report)?; + Ok(()) +} + +/// Removes a site's isolated cookie store (on site removal). +pub fn clear_site_data(app: &AppHandle, site_id: &Uuid) { + #[cfg(not(target_os = "macos"))] + if let Ok(dir) = app.path().app_local_data_dir() { + std::fs::remove_dir_all(dir.join("editor-sessions").join(site_id.to_string())).ok(); + } + #[cfg(target_os = "macos")] + { + // WKWebsiteDataStore removal needs the store to be unused; done at next launch. + let _ = (app, site_id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn site() -> SiteRecord { + let url = Url::parse("https://example.com/").unwrap(); + SiteRecord { + id: Uuid::new_v4(), + name: "x".into(), + admin_url: url.join("wp-admin/").unwrap(), + api_root: url.join("wp-json/").unwrap(), + url, + username: "eli".into(), + display_name: "Eli".into(), + avatar_url: None, + app_password_uuid: None, + bridge_available: false, + allow_insecure_local: false, + added_at: chrono::Utc::now(), + } + } + + #[test] + fn editor_urls() { + let s = site(); + assert_eq!( + editor_target(&s, "post", Some(42)).unwrap().as_str(), + "https://example.com/wp-admin/post.php?post=42&action=edit" + ); + assert_eq!( + editor_target(&s, "page", None).unwrap().as_str(), + "https://example.com/wp-admin/post-new.php?post_type=page" + ); + assert!(editor_target(&s, "page&x=1", None).is_err()); + } + + #[test] + fn report_validation() { + let r: EditorReport = serde_json::from_value(serde_json::json!({ + "kind": "editor", "title": "x".repeat(900), "link": "javascript:alert(1)" + })) + .unwrap(); + let r = r.validate().unwrap(); + assert_eq!(r.title.unwrap().len(), 500); + assert!(r.link.is_none()); + let bad: Result = + serde_json::from_value(serde_json::json!({"kind":"editor","evil":1})); + assert!(bad.is_err()); + let bad_kind: EditorReport = + serde_json::from_value(serde_json::json!({"kind":"x"})).unwrap(); + assert!(bad_kind.validate().is_err()); + } + + #[test] + fn bounds_are_clamped() { + let b = Bounds { + x: f64::NAN, + y: -5.0, + width: 0.0, + height: 1e9, + } + .sane(); + assert_eq!((b.x, b.y, b.width, b.height), (0.0, 0.0, 1.0, 20_000.0)); + } +} diff --git a/src-tauri/src/editor_bridge.js b/src-tauri/src/editor_bridge.js new file mode 100644 index 0000000..d35fe5a --- /dev/null +++ b/src-tauri/src/editor_bridge.js @@ -0,0 +1,163 @@ +// Injected into the editor webview (main frame only). It runs inside the +// site's own wp-admin, so it must treat the page as untrusted: it only reads +// editor state and reports a small, fixed-shape object to the native shell. +(function () { + "use strict"; + var CONFIG = __WPRE_CONFIG__; + if (window.top !== window) return; + if (CONFIG.origins.indexOf(window.location.origin) === -1) return; + + // ---- Native look: hide wp-admin chrome around the block editor ---------- + var css = + "html.wp-toolbar{padding-top:0!important}" + + "#wpadminbar,#adminmenumain,#wpfooter,.edit-post-fullscreen-mode-close," + + ".editor-header__back-button,.edit-post-header__back-button{display:none!important}" + + "#wpcontent,#wpbody-content{margin-left:0!important;padding-bottom:0!important}" + + ".interface-interface-skeleton{top:0!important;left:0!important}" + + "body.is-fullscreen-mode .interface-interface-skeleton{top:0!important}"; + function injectStyle() { + if (document.getElementById("wpre-embedded-style")) return; + var style = document.createElement("style"); + style.id = "wpre-embedded-style"; + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + document.documentElement.classList.add("wpre-embedded"); + } + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", injectStyle); + } else { + injectStyle(); + } + + // ---- Reporting -------------------------------------------------------- + function report(payload) { + try { + var ipc = window.__TAURI_INTERNALS__; + if (ipc && typeof ipc.invoke === "function") { + ipc.invoke("editor_report", { report: payload }); + } + } catch (e) { + /* the shell simply won't update */ + } + } + + var path = window.location.pathname; + if (/\/wp-login\.php$/.test(path)) { + report({ kind: "login" }); + return; + } + + function editorStore() { + var wp = window.wp; + return wp && wp.data && wp.data.select && wp.data.select("core/editor"); + } + + function hasPublishAction(s) { + var post = s.getCurrentPost && s.getCurrentPost(); + return !!(post && post._links && post._links["wp:action-publish"]); + } + + function snapshot(s) { + var title = s.getEditedPostAttribute("title"); + return { + kind: "editor", + postId: s.getCurrentPostId() || null, + postType: String(s.getCurrentPostType() || "").slice(0, 64), + title: String(title || "").slice(0, 500), + status: String(s.getEditedPostAttribute("status") || "").slice(0, 32), + link: String(s.getPermalink ? s.getPermalink() || "" : "").slice(0, 2048), + dirty: !!s.isEditedPostDirty(), + saving: !!s.isSavingPost() && !s.isAutosavingPost(), + canPublish: hasPublishAction(s), + }; + } + + function start() { + var s = editorStore(); + if (!s || !s.getCurrentPostId) return false; + var last = ""; + var pending = false; + window.wp.data.subscribe(function () { + if (pending) return; + pending = true; + // Coalesce bursts of store updates into one report per frame. + window.requestAnimationFrame(function () { + pending = false; + var snap = snapshot(editorStore()); + var key = JSON.stringify(snap); + if (key !== last) { + last = key; + report(snap); + } + }); + }); + report(snapshot(s)); + return true; + } + + // ---- Commands the native toolbar can trigger -------------------------- + function click(selectors) { + for (var i = 0; i < selectors.length; i++) { + var el = document.querySelector(selectors[i]); + if (el && !el.disabled && el.getAttribute("aria-disabled") !== "true") { + el.click(); + return true; + } + } + return false; + } + + Object.defineProperty(window, "__wpre", { + configurable: false, + writable: false, + value: Object.freeze({ + save: function () { + var d = window.wp.data.dispatch("core/editor"); + d.savePost(); + }, + // Use Gutenberg's own publish button so pre-publish checks and any + // plugin hooks on it (SEO, scheduling, review) still run. + publish: function () { + if ( + !click([ + ".editor-post-publish-panel__toggle", + ".editor-post-publish-button__button", + ".editor-post-publish-button", + ]) + ) { + var d = window.wp.data.dispatch("core/editor"); + var s = editorStore(); + d.editPost({ status: hasPublishAction(s) ? "publish" : "pending" }); + d.savePost(); + } + }, + device: function (type) { + var allowed = { Desktop: 1, Tablet: 1, Mobile: 1 }; + if (!allowed[type]) return; + var d = window.wp.data.dispatch("core/editor"); + if (d.setDeviceType) d.setDeviceType(type); + }, + undo: function () { + window.wp.data.dispatch("core/editor").undo(); + }, + redo: function () { + window.wp.data.dispatch("core/editor").redo(); + }, + toggleSettings: function () { + click([ + 'button[aria-controls="edit-post:document"]', + 'button[aria-label="Settings"]', + ]); + }, + }), + }); + + var tries = 0; + var timer = window.setInterval(function () { + tries += 1; + if (start() || tries > 120) { + window.clearInterval(timer); + if (tries > 120) report({ kind: "page", path: path.slice(0, 512) }); + } + }, 250); +})(); diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs new file mode 100644 index 0000000..f0b4081 --- /dev/null +++ b/src-tauri/src/error.rs @@ -0,0 +1,69 @@ +use serde::{Serialize, Serializer}; + +/// Errors returned to the shell UI. Messages are written for end users and +/// never include credentials, tokens or raw response bodies. +#[derive(Debug, thiserror::Error)] +pub enum AppError { + #[error("That doesn't look like a valid site address.")] + InvalidUrl, + #[error("This site must use HTTPS. Plain HTTP is only allowed for local development sites.")] + InsecureUrl, + #[error("Couldn't find the WordPress REST API on this site.")] + NotWordPress, + #[error("This site has Application Passwords turned off, so the app can't sign in. Ask the site administrator to enable them.")] + ApplicationPasswordsUnavailable, + #[error("WordPress rejected the username or application password.")] + Unauthorized, + #[error("Your WordPress role doesn't allow this action.")] + Forbidden, + #[error("The item no longer exists on the site.")] + NotFound, + #[error("WordPress returned an error: {0}")] + WordPress(String), + #[error("Couldn't reach the site: {0}")] + Network(String), + #[error("The site sent a response that was too large or malformed.")] + BadResponse, + #[error("Couldn't access the system keychain: {0}")] + Keychain(String), + #[error("Unknown site.")] + UnknownSite, + #[error("Sign-in was cancelled or didn't match the request.")] + AuthFlow, + #[error("{0}")] + Internal(String), +} + +impl Serialize for AppError { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl From for AppError { + fn from(e: reqwest::Error) -> Self { + // Strip the URL so query strings never reach logs or the UI. + let e = e.without_url(); + if e.is_timeout() { + AppError::Network("the connection timed out".into()) + } else if e.is_decode() { + AppError::BadResponse + } else { + AppError::Network(e.to_string()) + } + } +} + +impl From for AppError { + fn from(e: keyring::Error) -> Self { + AppError::Keychain(e.to_string()) + } +} + +impl From for AppError { + fn from(e: tauri::Error) -> Self { + AppError::Internal(e.to_string()) + } +} + +pub type AppResult = Result; diff --git a/src-tauri/src/keychain.rs b/src-tauri/src/keychain.rs new file mode 100644 index 0000000..ec12424 --- /dev/null +++ b/src-tauri/src/keychain.rs @@ -0,0 +1,100 @@ +//! Application Passwords live only in the OS keychain: +//! macOS Keychain, Windows Credential Manager, or Secret Service on Linux. + +use crate::error::{AppError, AppResult}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +const SERVICE: &str = "com.wpremoteeditor.app"; + +#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct Credentials { + pub username: String, + pub app_password: String, +} + +impl Credentials { + /// Application Passwords are shown with spaces for readability; WordPress + /// accepts them either way, so store the compact form. + pub fn new(username: &str, app_password: &str) -> Self { + Self { + username: username.trim().to_string(), + app_password: app_password + .chars() + .filter(|c| !c.is_whitespace()) + .collect(), + } + } + + /// `Basic` authorization header value. + pub fn basic_auth_header(&self) -> Zeroizing { + use base64::Engine; + let raw = Zeroizing::new(format!("{}:{}", self.username, self.app_password)); + Zeroizing::new(format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()) + )) + } +} + +impl std::fmt::Debug for Credentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Credentials") + .field("username", &self.username) + .field("app_password", &"") + .finish() + } +} + +fn entry(site_id: &Uuid) -> AppResult { + Ok(keyring::Entry::new(SERVICE, &format!("site:{site_id}"))?) +} + +pub fn save(site_id: &Uuid, creds: &Credentials) -> AppResult<()> { + let json = Zeroizing::new( + serde_json::to_string(creds).map_err(|e| AppError::Internal(e.to_string()))?, + ); + entry(site_id)?.set_password(&json)?; + Ok(()) +} + +pub fn load(site_id: &Uuid) -> AppResult { + let json = Zeroizing::new(entry(site_id)?.get_password().map_err(|e| match e { + keyring::Error::NoEntry => AppError::Unauthorized, + other => other.into(), + })?); + serde_json::from_str(&json) + .map_err(|_| AppError::Keychain("stored credentials are unreadable".into())) +} + +pub fn delete(site_id: &Uuid) -> AppResult<()> { + match entry(site_id)?.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(e.into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strips_spaces_from_application_passwords() { + let c = Credentials::new(" eli ", "abcd EFGH ijkl MNOP qrst UVWX"); + assert_eq!(c.username, "eli"); + assert_eq!(c.app_password, "abcdEFGHijklMNOPqrstUVWX"); + } + + #[test] + fn debug_output_redacts_password() { + let c = Credentials::new("eli", "secret"); + assert!(!format!("{c:?}").contains("secret")); + } + + #[test] + fn builds_basic_auth_header() { + let c = Credentials::new("eli", "pw"); + assert_eq!(c.basic_auth_header().as_str(), "Basic ZWxpOnB3"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..e851f20 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,61 @@ +mod auth; +mod commands; +mod editor; +mod error; +mod keychain; +mod menu; +mod security; +mod sites; +mod wp; + +use std::sync::Mutex; +use tauri::Manager; + +pub struct AppState { + pub store: sites::SiteStore, + pub pending_auth: Mutex>, + pub editor: Mutex>, +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(w) = app.get_webview_window("main") { + w.unminimize().ok(); + w.set_focus().ok(); + } + })) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_dialog::init()) + .setup(|app| { + let data_dir = app.path().app_data_dir()?; + app.manage(AppState { + store: sites::SiteStore::load(data_dir.join("sites.json")), + pending_auth: Mutex::new(None), + editor: Mutex::new(None), + }); + let menu = menu::build(app.handle())?; + app.set_menu(menu)?; + app.on_menu_event(|app, event| menu::forward(app, event.id().as_ref())); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::discover_site, + commands::begin_authorization, + commands::add_site_manual, + commands::list_sites, + commands::remove_site, + commands::get_site_context, + commands::list_posts, + commands::trash_post, + commands::open_editor, + commands::close_editor, + commands::editor_set_bounds, + commands::editor_command, + commands::editor_report, + ]) + .run(tauri::generate_context!()) + .expect("error while running WordPress Remote Editor"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..06a51b6 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an additional console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + wpre_lib::run(); +} diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs new file mode 100644 index 0000000..b152a3d --- /dev/null +++ b/src-tauri/src/menu.rs @@ -0,0 +1,96 @@ +//! Native application menu. Standard Edit items matter on macOS: without them +//! clipboard shortcuts don't reach the webviews. + +use tauri::menu::{ + AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder, +}; +use tauri::{AppHandle, Emitter, Runtime}; + +pub fn build(app: &AppHandle) -> tauri::Result> { + let item = |id: &str, label: &str, accel: Option<&str>| { + let mut b = MenuItemBuilder::with_id(id, label); + if let Some(a) = accel { + b = b.accelerator(a); + } + b.build(app) + }; + + let file = SubmenuBuilder::new(app, "File") + .item(&item("new-post", "New Post", Some("CmdOrCtrl+N"))?) + .item(&item("new-page", "New Page", Some("CmdOrCtrl+Shift+N"))?) + .separator() + .item(&item("save", "Save Draft", Some("CmdOrCtrl+S"))?) + .item(&item("publish", "Publish…", Some("CmdOrCtrl+Shift+P"))?) + .separator() + .item(&item("add-site", "Add Site…", None)?) + .item(&item("close-editor", "Close Editor", Some("CmdOrCtrl+W"))?); + #[cfg(not(target_os = "macos"))] + let file = file.separator().item(&PredefinedMenuItem::quit(app, None)?); + let file = file.build()?; + + let edit = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .build()?; + + let view = SubmenuBuilder::new(app, "View") + .item(&item( + "toggle-sidebar", + "Toggle Sidebar", + Some("CmdOrCtrl+Alt+S"), + )?) + .item(&item( + "toggle-settings", + "Toggle Block Settings", + Some("CmdOrCtrl+Shift+,"), + )?) + .separator() + .item(&item("device-desktop", "Preview Desktop", None)?) + .item(&item("device-tablet", "Preview Tablet", None)?) + .item(&item("device-mobile", "Preview Mobile", None)?) + .separator() + .fullscreen() + .build()?; + + let window = SubmenuBuilder::new(app, "Window") + .minimize() + .maximize() + .separator() + .close_window() + .build()?; + + let help = SubmenuBuilder::new(app, "Help") + .item(&item("help-docs", "WordPress Remote Editor Help", None)?) + .build()?; + + #[allow(unused_mut)] + let mut menu = MenuBuilder::new(app); + #[cfg(target_os = "macos")] + { + let app_menu = SubmenuBuilder::new(app, "WordPress Remote Editor") + .about(Some(AboutMetadata::default())) + .separator() + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build()?; + menu = menu.item(&app_menu); + } + #[cfg(not(target_os = "macos"))] + let _ = AboutMetadata::default(); + menu.items(&[&file, &edit, &view, &window, &help]).build() +} + +/// Forwards custom menu items to the shell as `menu://action` events. +pub fn forward(app: &AppHandle, id: &str) { + app.emit_to("main", "menu://action", id).ok(); +} diff --git a/src-tauri/src/security.rs b/src-tauri/src/security.rs new file mode 100644 index 0000000..b073825 --- /dev/null +++ b/src-tauri/src/security.rs @@ -0,0 +1,122 @@ +//! URL normalisation and origin rules shared by the REST client, the auth +//! window and the editor webview. + +use crate::error::{AppError, AppResult}; +use url::Url; + +/// Hosts where plain HTTP is acceptable (local development only). +pub fn is_local_dev_host(host: &str) -> bool { + let host = host.trim_end_matches('.').to_ascii_lowercase(); + host == "localhost" + || host == "127.0.0.1" + || host == "[::1]" + || host == "::1" + || host.ends_with(".localhost") + || host.ends_with(".local") + || host.ends_with(".test") +} + +/// Turns what the user typed ("example.com", "https://example.com/blog/") +/// into a canonical site URL with no query, fragment or credentials. +pub fn normalize_site_url(input: &str, allow_insecure_local: bool) -> AppResult { + let trimmed = input.trim(); + if trimmed.is_empty() || trimmed.len() > 2048 { + return Err(AppError::InvalidUrl); + } + let with_scheme = if trimmed.contains("://") { + trimmed.to_string() + } else { + format!("https://{trimmed}") + }; + let mut url = Url::parse(&with_scheme).map_err(|_| AppError::InvalidUrl)?; + if !url.username().is_empty() || url.password().is_some() { + return Err(AppError::InvalidUrl); + } + let host = url.host_str().ok_or(AppError::InvalidUrl)?.to_string(); + match url.scheme() { + "https" => {} + "http" if allow_insecure_local && is_local_dev_host(&host) => {} + "http" => return Err(AppError::InsecureUrl), + _ => return Err(AppError::InvalidUrl), + } + url.set_query(None); + url.set_fragment(None); + // Strip common admin paths people paste by accident. + let mut path = url.path().trim_end_matches('/').to_string(); + for suffix in ["/wp-admin", "/wp-login.php", "/wp-json"] { + if let Some(stripped) = path.strip_suffix(suffix) { + path = stripped.to_string(); + } + } + url.set_path(&format!("{path}/")); + Ok(url) +} + +/// True when `candidate` has the same scheme, host and port as `origin`. +pub fn same_origin(origin: &Url, candidate: &Url) -> bool { + origin.scheme() == candidate.scheme() + && origin.host_str() == candidate.host_str() + && origin.port_or_known_default() == candidate.port_or_known_default() +} + +/// Rejects a URL handed to us by a server (e.g. the authorization endpoint or +/// the REST root) unless it stays on the same origin as the site and is not a +/// downgrade. +pub fn require_same_origin(site: &Url, candidate: &str) -> AppResult { + let url = Url::parse(candidate).map_err(|_| AppError::BadResponse)?; + if same_origin(site, &url) { + return Ok(url); + } + // Allow http -> https upgrade on the same host. + if site.scheme() == "http" && url.scheme() == "https" && site.host_str() == url.host_str() { + return Ok(url); + } + Err(AppError::BadResponse) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adds_https_and_trailing_slash() { + let u = normalize_site_url("example.com", false).unwrap(); + assert_eq!(u.as_str(), "https://example.com/"); + } + + #[test] + fn strips_admin_paths_query_and_fragment() { + let u = normalize_site_url("https://example.com/blog/wp-admin/?x=1#y", false).unwrap(); + assert_eq!(u.as_str(), "https://example.com/blog/"); + } + + #[test] + fn rejects_plain_http_for_public_hosts() { + assert!(matches!( + normalize_site_url("http://example.com", true), + Err(AppError::InsecureUrl) + )); + } + + #[test] + fn allows_http_for_local_dev_only_when_opted_in() { + assert!(normalize_site_url("http://mysite.test", true).is_ok()); + assert!(normalize_site_url("http://mysite.test", false).is_err()); + } + + #[test] + fn rejects_credentials_and_odd_schemes() { + assert!(normalize_site_url("https://user:pw@example.com", false).is_err()); + assert!(normalize_site_url("javascript:alert(1)", false).is_err()); + assert!(normalize_site_url("file:///etc/passwd", false).is_err()); + } + + #[test] + fn origin_checks() { + let site = Url::parse("https://example.com/").unwrap(); + assert!(require_same_origin(&site, "https://example.com/wp-json/").is_ok()); + assert!(require_same_origin(&site, "https://evil.com/wp-json/").is_err()); + assert!(require_same_origin(&site, "http://example.com/wp-json/").is_err()); + assert!(require_same_origin(&site, "https://example.com:8443/").is_err()); + } +} diff --git a/src-tauri/src/sites.rs b/src-tauri/src/sites.rs new file mode 100644 index 0000000..8c28970 --- /dev/null +++ b/src-tauri/src/sites.rs @@ -0,0 +1,166 @@ +//! Non-secret site metadata, persisted as JSON in the app data directory. +//! Secrets are never written here; see `keychain`. + +use crate::error::{AppError, AppResult}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::sync::Mutex; +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteRecord { + pub id: Uuid, + pub name: String, + pub url: Url, + /// `siteurl` + `wp-admin/`, where the block editor is loaded from. + pub admin_url: Url, + pub api_root: Url, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + /// UUID of the Application Password, used to revoke it on removal. + pub app_password_uuid: Option, + /// Whether the companion plugin is active (enables editor sign-in handoff). + pub bridge_available: bool, + pub allow_insecure_local: bool, + pub added_at: chrono::DateTime, +} + +impl SiteRecord { + pub fn summary(&self) -> crate::wp::models::SiteSummary { + crate::wp::models::SiteSummary { + id: self.id, + name: self.name.clone(), + url: self.url.clone(), + username: self.username.clone(), + display_name: self.display_name.clone(), + avatar_url: self.avatar_url.clone(), + bridge_available: self.bridge_available, + } + } +} + +pub struct SiteStore { + path: PathBuf, + sites: Mutex>, +} + +impl SiteStore { + pub fn load(path: PathBuf) -> Self { + let sites = std::fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice(&bytes).ok()) + .unwrap_or_default(); + Self { + path, + sites: Mutex::new(sites), + } + } + + pub fn list(&self) -> Vec { + self.sites.lock().expect("site store poisoned").clone() + } + + pub fn get(&self, id: &Uuid) -> AppResult { + self.list() + .into_iter() + .find(|s| &s.id == id) + .ok_or(AppError::UnknownSite) + } + + /// Inserts or replaces (matched by URL + username so re-adding a site + /// doesn't create duplicates). + pub fn upsert(&self, mut record: SiteRecord) -> AppResult { + let mut sites = self.sites.lock().expect("site store poisoned"); + if let Some(existing) = sites + .iter_mut() + .find(|s| s.url == record.url && s.username == record.username) + { + record.id = existing.id; + record.added_at = existing.added_at; + *existing = record.clone(); + } else { + sites.push(record.clone()); + } + self.persist(&sites)?; + Ok(record) + } + + pub fn remove(&self, id: &Uuid) -> AppResult<()> { + let mut sites = self.sites.lock().expect("site store poisoned"); + sites.retain(|s| &s.id != id); + self.persist(&sites) + } + + fn persist(&self, sites: &[SiteRecord]) -> AppResult<()> { + let io = |e: std::io::Error| AppError::Internal(format!("couldn't save site list: {e}")); + if let Some(dir) = self.path.parent() { + std::fs::create_dir_all(dir).map_err(io)?; + } + let tmp = self.path.with_extension("json.tmp"); + let bytes = + serde_json::to_vec_pretty(sites).map_err(|e| AppError::Internal(e.to_string()))?; + write_private(&tmp, &bytes).map_err(io)?; + std::fs::rename(&tmp, &self.path).map_err(io) + } +} + +#[cfg(unix)] +fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + let mut f = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path)?; + f.write_all(bytes)?; + f.sync_all() +} + +#[cfg(not(unix))] +fn write_private(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> { + // The per-user app data directory on Windows is already ACL'd to the user. + std::fs::write(path, bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn record(url: &str, user: &str) -> SiteRecord { + let url = Url::parse(url).unwrap(); + SiteRecord { + id: Uuid::new_v4(), + name: "Site".into(), + api_root: url.join("wp-json/").unwrap(), + admin_url: url.join("wp-admin/").unwrap(), + url, + username: user.into(), + display_name: user.into(), + avatar_url: None, + app_password_uuid: None, + bridge_available: false, + allow_insecure_local: false, + added_at: chrono::Utc::now(), + } + } + + #[test] + fn upsert_dedupes_and_persists() { + let dir = std::env::temp_dir().join(format!("wpre-test-{}", Uuid::new_v4())); + let path = dir.join("sites.json"); + let store = SiteStore::load(path.clone()); + let a = store.upsert(record("https://a.com/", "eli")).unwrap(); + let again = store.upsert(record("https://a.com/", "eli")).unwrap(); + assert_eq!(a.id, again.id); + store.upsert(record("https://b.com/", "eli")).unwrap(); + assert_eq!(SiteStore::load(path).list().len(), 2); + store.remove(&a.id).unwrap(); + assert_eq!(store.list().len(), 1); + std::fs::remove_dir_all(dir).ok(); + } +} diff --git a/src-tauri/src/wp/client.rs b/src-tauri/src/wp/client.rs new file mode 100644 index 0000000..66232c1 --- /dev/null +++ b/src-tauri/src/wp/client.rs @@ -0,0 +1,242 @@ +//! Authenticated WordPress REST client. All requests go through the Rust core; +//! the shell webview never sees credentials or talks to the site directly. + +use crate::error::{AppError, AppResult}; +use crate::keychain::Credentials; +use crate::security::{is_local_dev_host, same_origin}; +use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION}; +use reqwest::{Method, Response, StatusCode}; +use serde::de::DeserializeOwned; +use std::time::Duration; +use url::Url; +use zeroize::Zeroizing; + +pub const USER_AGENT: &str = concat!("WordPressRemoteEditor/", env!("CARGO_PKG_VERSION")); +const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024; +const MAX_REDIRECTS: usize = 5; + +/// Builds an HTTP client. `pin_origin` restricts redirects to that origin +/// (used for authenticated clients); `None` allows HTTPS redirects anywhere +/// (used for unauthenticated discovery, e.g. example.com -> www.example.com). +pub fn http_client( + pin_origin: Option, + allow_insecure_local: bool, +) -> AppResult { + let policy = reqwest::redirect::Policy::custom(move |attempt| { + if attempt.previous().len() >= MAX_REDIRECTS { + return attempt.error("too many redirects"); + } + let next = attempt.url(); + let ok_scheme = next.scheme() == "https" + || (allow_insecure_local + && next.scheme() == "http" + && next.host_str().is_some_and(is_local_dev_host)); + if !ok_scheme { + return attempt.stop(); + } + match &pin_origin { + Some(origin) if !same_origin(origin, next) => attempt.stop(), + _ => attempt.follow(), + } + }); + reqwest::Client::builder() + .user_agent(USER_AGENT) + .https_only(!allow_insecure_local) + .redirect(policy) + .connect_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(60)) + .build() + .map_err(AppError::from) +} + +pub async fn read_limited(mut resp: Response) -> AppResult> { + if resp + .content_length() + .is_some_and(|l| l as usize > MAX_RESPONSE_BYTES) + { + return Err(AppError::BadResponse); + } + let mut buf = Vec::new(); + while let Some(chunk) = resp.chunk().await? { + if buf.len() + chunk.len() > MAX_RESPONSE_BYTES { + return Err(AppError::BadResponse); + } + buf.extend_from_slice(&chunk); + } + Ok(buf) +} + +/// Maps a non-success response to a user-facing error, using WordPress's +/// `{ code, message }` error body when present. +pub async fn error_for(resp: Response) -> AppError { + let status = resp.status(); + let body = read_limited(resp).await.unwrap_or_default(); + let wp: Option = serde_json::from_slice(&body).ok(); + match status { + StatusCode::UNAUTHORIZED => AppError::Unauthorized, + StatusCode::FORBIDDEN => AppError::Forbidden, + StatusCode::NOT_FOUND | StatusCode::GONE => AppError::NotFound, + _ => match wp { + Some(e) if !e.message.is_empty() => { + let mut msg: String = strip_tags(&e.message).chars().take(300).collect(); + if msg.is_empty() { + msg = e.code; + } + AppError::WordPress(msg) + } + _ => AppError::WordPress(format!("HTTP {}", status.as_u16())), + }, + } +} + +fn strip_tags(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut in_tag = false; + for c in s.chars() { + match c { + '<' => in_tag = true, + '>' => in_tag = false, + _ if !in_tag => out.push(c), + _ => {} + } + } + out +} + +/// Builds a REST URL for `route` (e.g. `wp/v2/posts`) under `api_root`, +/// supporting both pretty permalinks (`/wp-json/`) and plain permalinks +/// (`/?rest_route=/`). +pub fn route_url(api_root: &Url, route: &str, query: &[(&str, String)]) -> AppResult { + let route = route.trim_start_matches('/'); + let uses_rest_route = api_root.query_pairs().any(|(k, _)| k == "rest_route"); + let mut url = if uses_rest_route { + let mut base = api_root.clone(); + base.set_query(None); + base.query_pairs_mut() + .append_pair("rest_route", &format!("/{route}")); + base + } else { + let mut root = api_root.clone(); + if !root.path().ends_with('/') { + root.set_path(&format!("{}/", root.path())); + } + root.join(route).map_err(|_| AppError::InvalidUrl)? + }; + if !query.is_empty() { + let mut pairs = url.query_pairs_mut(); + for (k, v) in query { + pairs.append_pair(k, v); + } + } + Ok(url) +} + +pub struct WpClient { + http: reqwest::Client, + api_root: Url, + auth: Zeroizing, +} + +impl WpClient { + pub fn new(api_root: Url, creds: &Credentials, allow_insecure_local: bool) -> AppResult { + Ok(Self { + http: http_client(Some(api_root.clone()), allow_insecure_local)?, + auth: creds.basic_auth_header(), + api_root, + }) + } + + async fn send( + &self, + method: Method, + route: &str, + query: &[(&str, String)], + body: Option<&serde_json::Value>, + ) -> AppResult<(Vec, HeaderMap)> { + let url = route_url(&self.api_root, route, query)?; + let mut req = self + .http + .request(method, url) + .header(AUTHORIZATION, self.auth.as_str()) + .header(ACCEPT, "application/json"); + if let Some(b) = body { + req = req.json(b); + } + let resp = req.send().await?; + if !resp.status().is_success() { + return Err(error_for(resp).await); + } + let headers = resp.headers().clone(); + Ok((read_limited(resp).await?, headers)) + } + + pub async fn get( + &self, + route: &str, + query: &[(&str, String)], + ) -> AppResult<(T, HeaderMap)> { + let (bytes, headers) = self.send(Method::GET, route, query, None).await?; + let value = serde_json::from_slice(&bytes).map_err(|_| AppError::BadResponse)?; + Ok((value, headers)) + } + + pub async fn post( + &self, + route: &str, + body: &serde_json::Value, + ) -> AppResult { + let (bytes, _) = self.send(Method::POST, route, &[], Some(body)).await?; + serde_json::from_slice(&bytes).map_err(|_| AppError::BadResponse) + } + + pub async fn delete(&self, route: &str, query: &[(&str, String)]) -> AppResult<()> { + self.send(Method::DELETE, route, query, None) + .await + .map(|_| ()) + } +} + +pub fn header_u64(headers: &HeaderMap, name: &str) -> u64 { + headers + .get(name) + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pretty_permalink_routes() { + let root = Url::parse("https://example.com/wp-json/").unwrap(); + let u = route_url(&root, "wp/v2/posts", &[("context", "edit".into())]).unwrap(); + assert_eq!( + u.as_str(), + "https://example.com/wp-json/wp/v2/posts?context=edit" + ); + } + + #[test] + fn plain_permalink_routes() { + let root = Url::parse("https://example.com/?rest_route=/").unwrap(); + let u = route_url(&root, "/wp/v2/posts", &[("page", "2".into())]).unwrap(); + assert_eq!( + u.as_str(), + "https://example.com/?rest_route=%2Fwp%2Fv2%2Fposts&page=2" + ); + } + + #[test] + fn query_values_are_encoded() { + let root = Url::parse("https://example.com/wp-json/").unwrap(); + let u = route_url(&root, "wp/v2/posts", &[("search", "a&b=c".into())]).unwrap(); + assert!(u.as_str().ends_with("search=a%26b%3Dc")); + } + + #[test] + fn strips_html_from_error_messages() { + assert_eq!(strip_tags("Error: nope"), "Error: nope"); + } +} diff --git a/src-tauri/src/wp/discovery.rs b/src-tauri/src/wp/discovery.rs new file mode 100644 index 0000000..657050e --- /dev/null +++ b/src-tauri/src/wp/discovery.rs @@ -0,0 +1,136 @@ +//! Finds a site's REST API and Application Password authorization endpoint +//! the way WordPress documents it: via the `rel="https://api.w.org/"` link. + +use super::client::{error_for, http_client, read_limited}; +use super::models::{RestIndex, SiteDiscovery}; +use crate::error::{AppError, AppResult}; +use crate::security::{normalize_site_url, require_same_origin}; +use reqwest::header::LINK; +use url::Url; + +const API_REL: &str = "https://api.w.org/"; + +/// Extracts the REST API root from one or more `Link` header values. +pub fn api_root_from_link_headers<'a>(values: impl Iterator) -> Option { + for value in values { + for part in value.split(',') { + let mut segments = part.split(';'); + let target = segments.next()?.trim(); + let is_api = segments.any(|s| { + let s = s.trim(); + s == format!("rel=\"{API_REL}\"") || s == format!("rel={API_REL}") + }); + if is_api { + let t = target.trim_start_matches('<').trim_end_matches('>'); + return Some(t.to_string()); + } + } + } + None +} + +pub async fn discover(input: &str, allow_insecure_local: bool) -> AppResult { + let entered = normalize_site_url(input, allow_insecure_local)?; + let http = http_client(None, allow_insecure_local)?; + + // 1. Load the site and follow redirects (e.g. to www.) to learn its real address. + let resp = http.get(entered.clone()).send().await?; + let site_url = normalize_site_url(resp.url().as_str(), allow_insecure_local)?; + let link = api_root_from_link_headers( + resp.headers() + .get_all(LINK) + .iter() + .filter_map(|v| v.to_str().ok()), + ); + drop(resp); + + let api_root = match link { + Some(l) => require_same_origin(&site_url, &l)?, + None => site_url + .join("wp-json/") + .map_err(|_| AppError::InvalidUrl)?, + }; + + // 2. Read the REST index. + let resp = http + .get(api_root.clone()) + .header(reqwest::header::ACCEPT, "application/json") + .send() + .await?; + if !resp.status().is_success() { + return Err(match error_for(resp).await { + AppError::NotFound => AppError::NotWordPress, + e => e, + }); + } + let bytes = read_limited(resp).await?; + let index: RestIndex = serde_json::from_slice(&bytes).map_err(|_| AppError::NotWordPress)?; + if !index.namespaces.iter().any(|n| n == "wp/v2") { + return Err(AppError::NotWordPress); + } + + // `url` (siteurl) is where wp-admin lives and may differ from `home`. + // It must use an acceptable scheme, but may be on a different host. + let admin_base = if index.url.is_empty() { + site_url.clone() + } else { + normalize_site_url(&index.url, allow_insecure_local)? + }; + let admin_url = admin_base + .join("wp-admin/") + .map_err(|_| AppError::BadResponse)?; + + let authorization_endpoint = index + .authentication + .get("application-passwords") + .and_then(|v| v.pointer("/endpoints/authorization")) + .and_then(|v| v.as_str()) + .and_then(|s| Url::parse(s).ok()) + .filter(|u| u.scheme() == "https" || (allow_insecure_local && u.scheme() == "http")); + + Ok(SiteDiscovery { + name: if index.name.is_empty() { + site_url.host_str().unwrap_or_default().to_string() + } else { + index.name + }, + description: index.description, + site_url, + admin_url, + api_root, + supports_application_passwords: authorization_endpoint.is_some(), + authorization_endpoint, + bridge_available: index.namespaces.iter().any(|n| n == "wpre/v1"), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_api_link_among_several() { + let headers = [ + "; rel=\"alternate\"; type=\"application/json\"", + "; rel=\"https://api.w.org/\"", + ]; + assert_eq!( + api_root_from_link_headers(headers.iter().copied()).as_deref(), + Some("https://example.com/wp-json/") + ); + } + + #[test] + fn handles_comma_joined_links_and_plain_permalinks() { + let headers = ["; rel=shortlink, ; rel=\"https://api.w.org/\""]; + assert_eq!( + api_root_from_link_headers(headers.iter().copied()).as_deref(), + Some("https://e.com/?rest_route=/") + ); + } + + #[test] + fn none_when_absent() { + assert!(api_root_from_link_headers(["; rel=\"next\""].iter().copied()).is_none()); + } +} diff --git a/src-tauri/src/wp/mod.rs b/src-tauri/src/wp/mod.rs new file mode 100644 index 0000000..ccd6d98 --- /dev/null +++ b/src-tauri/src/wp/mod.rs @@ -0,0 +1,117 @@ +pub mod client; +pub mod discovery; +pub mod models; + +/// Live tests against a real WordPress site. Run with: +/// `WPRE_TEST_SITE=… WPRE_TEST_USER=… WPRE_TEST_APP_PASSWORD=… cargo test -- --ignored` +#[cfg(test)] +mod live_tests { + use super::client::WpClient; + use super::discovery::discover; + use super::models::{post_type_access, RestPost, RestPostType, RestUser}; + use crate::keychain::Credentials; + use std::collections::HashMap; + + fn env() -> Option<(String, String, String)> { + Some(( + std::env::var("WPRE_TEST_SITE").ok()?, + std::env::var("WPRE_TEST_USER").ok()?, + std::env::var("WPRE_TEST_APP_PASSWORD").ok()?, + )) + } + + #[tokio::test] + #[ignore = "needs a live WordPress site"] + async fn discovers_and_reads_as_the_user() { + let Some((site, user, pw)) = env() else { + return; + }; + let local = site.starts_with("http://"); + let d = discover(&site, local).await.expect("discovery"); + assert!(d.supports_application_passwords); + let client = + WpClient::new(d.api_root.clone(), &Credentials::new(&user, &pw), local).unwrap(); + + let (me, _) = client + .get::("wp/v2/users/me", &[("context", "edit".into())]) + .await + .expect("users/me"); + let (types, _) = client + .get::>("wp/v2/types", &[("context", "edit".into())]) + .await + .expect("types"); + let access: Vec<_> = types + .values() + .map(|t| post_type_access(t, &me.capabilities)) + .collect(); + println!( + "site={} bridge={} roles={:?}", + d.name, d.bridge_available, me.roles + ); + for a in &access { + println!( + " {} create={} publish={}", + a.slug, a.can_create, a.can_publish + ); + } + let (posts, headers) = client + .get::>( + "wp/v2/posts", + &[ + ("context", "edit".into()), + ("status", "any".into()), + ("per_page", "5".into()), + ], + ) + .await + .expect("posts"); + println!( + " posts={} total={:?}", + posts.len(), + headers.get("x-wp-total") + ); + + if d.bridge_available { + #[derive(serde::Deserialize)] + struct Handoff { + url: String, + } + let target = d.admin_url.join("post-new.php").unwrap(); + let h: Handoff = client + .post( + "wpre/v1/handoff", + &serde_json::json!({ "redirect_to": target.as_str() }), + ) + .await + .expect("handoff"); + let url = url::Url::parse(&h.url).unwrap(); + assert!(crate::security::same_origin(&d.admin_url, &url)); + assert!(url + .query_pairs() + .any(|(k, v)| k == "action" && v == "wpre_handoff")); + println!(" handoff ok"); + } + } + + #[tokio::test] + #[ignore = "needs a live WordPress site"] + async fn wrong_password_is_unauthorized() { + let Some((site, user, _)) = env() else { return }; + let local = site.starts_with("http://"); + let d = discover(&site, local).await.unwrap(); + let client = WpClient::new( + d.api_root, + &Credentials::new(&user, "wrong wrong wrong"), + local, + ) + .unwrap(); + let err = client + .get::("wp/v2/users/me", &[]) + .await + .unwrap_err(); + assert!( + matches!(err, crate::error::AppError::Unauthorized), + "{err:?}" + ); + } +} diff --git a/src-tauri/src/wp/models.rs b/src-tauri/src/wp/models.rs new file mode 100644 index 0000000..eecbb87 --- /dev/null +++ b/src-tauri/src/wp/models.rs @@ -0,0 +1,242 @@ +//! WordPress REST API shapes (only the fields the app uses) and the +//! camelCase DTOs sent to the shell UI. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +// ---- REST responses ------------------------------------------------------ + +#[derive(Debug, Deserialize)] +pub struct RestIndex { + #[serde(default)] + pub name: String, + #[serde(default)] + pub description: String, + /// `siteurl`: where wp-admin lives. + #[serde(default)] + pub url: String, + #[serde(default)] + pub namespaces: Vec, + #[serde(default)] + pub authentication: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct RestUser { + pub id: u64, + #[serde(default)] + pub name: String, + #[serde(default)] + pub avatar_urls: HashMap, + #[serde(default)] + pub roles: Vec, + #[serde(default)] + pub capabilities: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct RestPostType { + pub slug: String, + #[serde(default)] + pub name: String, + pub rest_base: String, + #[serde(default)] + pub rest_namespace: Option, + #[serde(default)] + pub viewable: bool, + #[serde(default)] + pub hierarchical: bool, + #[serde(default)] + pub capabilities: HashMap, + #[serde(default)] + pub labels: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct RestRendered { + #[serde(default)] + pub raw: Option, + #[serde(default)] + pub rendered: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RestPost { + pub id: u64, + #[serde(default)] + pub title: Option, + #[serde(default)] + pub status: String, + #[serde(default)] + pub modified: Option, + #[serde(default)] + pub author: Option, + #[serde(default)] + pub link: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RestErrorBody { + #[serde(default)] + pub code: String, + #[serde(default)] + pub message: String, +} + +// ---- DTOs for the shell UI ----------------------------------------------- + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteDiscovery { + pub site_url: Url, + pub admin_url: Url, + pub api_root: Url, + pub name: String, + pub description: String, + pub supports_application_passwords: bool, + #[serde(skip)] + pub authorization_endpoint: Option, + pub bridge_available: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteSummary { + pub id: uuid::Uuid, + pub name: String, + pub url: Url, + pub username: String, + pub display_name: String, + pub avatar_url: Option, + pub bridge_available: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostTypeAccess { + pub slug: String, + pub rest_base: String, + pub name: String, + pub singular_name: String, + pub hierarchical: bool, + pub can_create: bool, + pub can_publish: bool, + pub can_edit_others: bool, + pub can_delete: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SiteContext { + pub site: SiteSummary, + pub user_id: u64, + pub roles: Vec, + pub post_types: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostSummary { + pub id: u64, + pub title: String, + pub status: String, + pub modified: Option, + pub author: Option, + pub link: Option, + pub can_edit: bool, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PostPage { + pub items: Vec, + pub total: u64, + pub total_pages: u64, + pub page: u32, +} + +/// Works out what the user may do with a post type from their capability map +/// and the post type's capability names. WordPress enforces all of this on +/// the server too; this only decides what the UI offers. +pub fn post_type_access(t: &RestPostType, caps: &HashMap) -> PostTypeAccess { + let has = |key: &str, fallback: &str| { + let cap = t + .capabilities + .get(key) + .map(String::as_str) + .unwrap_or(fallback); + caps.get(cap).copied().unwrap_or(false) + }; + let singular = t + .labels + .get("singular_name") + .and_then(|v| v.as_str()) + .unwrap_or(&t.name) + .to_string(); + PostTypeAccess { + slug: t.slug.clone(), + rest_base: t.rest_base.clone(), + name: t.name.clone(), + singular_name: singular, + hierarchical: t.hierarchical, + can_create: has("create_posts", "edit_posts") && has("edit_posts", "edit_posts"), + can_publish: has("publish_posts", "publish_posts"), + can_edit_others: has("edit_others_posts", "edit_others_posts"), + can_delete: has("delete_posts", "delete_posts"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn page_type() -> RestPostType { + serde_json::from_value(serde_json::json!({ + "slug": "page", "name": "Pages", "rest_base": "pages", "viewable": true, + "hierarchical": true, + "capabilities": { + "edit_posts": "edit_pages", "create_posts": "edit_pages", + "publish_posts": "publish_pages", "edit_others_posts": "edit_others_pages", + "delete_posts": "delete_pages" + }, + "labels": { "singular_name": "Page" } + })) + .unwrap() + } + + #[test] + fn contributor_cannot_create_pages() { + let caps = HashMap::from([("edit_posts".to_string(), true)]); + let a = post_type_access(&page_type(), &caps); + assert!(!a.can_create); + assert!(!a.can_publish); + } + + #[test] + fn editor_can_do_everything_with_pages() { + let caps: HashMap = [ + "edit_pages", + "publish_pages", + "edit_others_pages", + "delete_pages", + ] + .into_iter() + .map(|c| (c.to_string(), true)) + .collect(); + let a = post_type_access(&page_type(), &caps); + assert!(a.can_create && a.can_publish && a.can_edit_others && a.can_delete); + assert_eq!(a.singular_name, "Page"); + } + + #[test] + fn false_capabilities_are_respected() { + let caps = HashMap::from([ + ("edit_pages".to_string(), true), + ("publish_pages".to_string(), false), + ]); + let a = post_type_access(&page_type(), &caps); + assert!(a.can_create); + assert!(!a.can_publish); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..e313b02 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "WordPress Remote Editor", + "mainBinaryName": "wordpress-remote-editor", + "version": "0.1.0", + "identifier": "com.wpremoteeditor.app", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "macOSPrivateApi": true, + "withGlobalTauri": false, + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false + } + ], + "security": { + "freezePrototype": true, + "csp": { + "default-src": "'self'", + "script-src": "'self'", + "style-src": "'self' 'unsafe-inline'", + "img-src": "'self' https: data:", + "font-src": "'self'", + "connect-src": "ipc: http://ipc.localhost", + "object-src": "'none'", + "frame-src": "'none'", + "base-uri": "'none'", + "form-action": "'none'" + } + } + }, + "bundle": { + "active": true, + "targets": [ + "app", + "dmg", + "deb", + "appimage", + "msi" + ], + "category": "Productivity", + "shortDescription": "A native desktop editor for WordPress sites", + "longDescription": "Write and publish posts and pages on your WordPress sites with the full block editor, including blocks from any plugin, in a native desktop app.", + "copyright": "\u00a9 WordPress Remote Editor contributors", + "license": "GPL-2.0-or-later", + "publisher": "WordPress Remote Editor", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "macOS": { + "minimumSystemVersion": "14.0", + "hardenedRuntime": true, + "entitlements": "entitlements.plist", + "dmg": { + "windowSize": { + "width": 660, + "height": 400 + }, + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + } + } + }, + "linux": { + "deb": { + "section": "editors", + "depends": [ + "libwebkit2gtk-4.1-0", + "libgtk-3-0", + "libsecret-1-0" + ] + } + }, + "windows": { + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com", + "webviewInstallMode": { + "type": "downloadBootstrapper", + "silent": true + }, + "wix": { + "language": "en-US" + } + } + } +} diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000..c1c9868 --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "transparent": true, + "trafficLightPosition": { + "x": 18, + "y": 22 + }, + "windowEffects": { + "effects": [ + "sidebar" + ], + "state": "followsWindowActiveState" + } + } + ] + } +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..319487f --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "app": { + "windows": [ + { + "label": "main", + "title": "WordPress Remote Editor", + "width": 1280, + "height": 820, + "minWidth": 900, + "minHeight": 600, + "center": true, + "dragDropEnabled": false, + "transparent": true, + "windowEffects": { "effects": ["mica"] } + } + ] + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..1771414 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,262 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { ask, message } from "@tauri-apps/plugin-dialog"; +import { api, errorMessage, type EditorReport, type PostTypeAccess, type SiteContext, type SiteSummary } from "./lib/api"; +import type { Platform } from "./lib/platform"; +import { AddSiteSheet } from "./components/AddSiteSheet"; +import { EditorView } from "./components/EditorView"; +import { Icon } from "./components/Icon"; +import { PostList } from "./components/PostList"; +import { Sidebar } from "./components/Sidebar"; + +type View = { kind: "list" } | { kind: "editor"; postId: number | null; key: number }; + +export function App({ platform }: { platform: Platform }) { + const [sites, setSites] = useState([]); + const [siteId, setSiteId] = useState(null); + const [context, setContext] = useState(null); + const [contextError, setContextError] = useState(null); + const [postType, setPostType] = useState(null); + const [view, setView] = useState({ kind: "list" }); + const [report, setReport] = useState(null); + const [adding, setAdding] = useState(false); + const [waitingForBrowser, setWaitingForBrowser] = useState(false); + const [authError, setAuthError] = useState(null); + const [reloadKey, setReloadKey] = useState(0); + + // Refs so native event handlers always see current state. + const state = useRef({ view, report, context, postType }); + state.current = { view, report, context, postType }; + + const refreshSites = useCallback(async () => { + const list = await api.listSites(); + setSites(list); + return list; + }, []); + + useEffect(() => { + void refreshSites().then((list) => list[0] && setSiteId((cur) => cur ?? list[0].id)); + }, [refreshSites]); + + // Load what the signed-in user can do on the selected site. + useEffect(() => { + setContext(null); + setPostType(null); + setContextError(null); + if (!siteId) return; + let cancelled = false; + api + .getSiteContext(siteId) + .then((ctx) => { + if (cancelled) return; + setContext(ctx); + setPostType(ctx.postTypes[0] ?? null); + }) + .catch((e) => !cancelled && setContextError(errorMessage(e))); + return () => { + cancelled = true; + }; + }, [siteId, reloadKey]); + + /** Leaves the editor, asking first if there are unsaved changes. */ + const leaveEditor = useCallback(async (): Promise => { + const { view, report } = state.current; + if (view.kind !== "editor") return true; + if (report?.dirty) { + const ok = await ask("You have unsaved changes. Leave the editor and discard them?", { + title: "Unsaved Changes", + kind: "warning", + okLabel: "Discard Changes", + cancelLabel: "Keep Editing", + }); + if (!ok) return false; + } + await api.closeEditor().catch(() => {}); + setReport(null); + setView({ kind: "list" }); + return true; + }, []); + + const openEditor = useCallback((postId: number | null) => { + setReport(null); + setView({ kind: "editor", postId, key: Date.now() }); + }, []); + + const selectSite = useCallback( + async (id: string) => { + if (await leaveEditor()) setSiteId(id); + }, + [leaveEditor], + ); + + const selectType = useCallback( + async (t: PostTypeAccess) => { + if (await leaveEditor()) setPostType(t); + }, + [leaveEditor], + ); + + const startAddSite = useCallback(async () => { + if (await leaveEditor()) { + setAuthError(null); + setWaitingForBrowser(false); + setAdding(true); + } + }, [leaveEditor]); + + const removeSite = useCallback( + async (site: SiteSummary) => { + const ok = await ask( + `Remove ${site.name} from this app? Its application password will be revoked on the site and deleted from your keychain.`, + { title: "Remove Site", kind: "warning", okLabel: "Remove", cancelLabel: "Cancel" }, + ); + if (!ok || !(await leaveEditor())) return; + try { + await api.removeSite(site.id); + } catch (e) { + await message(errorMessage(e), { title: "Couldn't remove site", kind: "error" }); + } + const list = await refreshSites(); + setSiteId(list[0]?.id ?? null); + }, + [leaveEditor, refreshSites], + ); + + const onSiteAdded = useCallback( + async (site: SiteSummary) => { + setAdding(false); + setWaitingForBrowser(false); + await refreshSites(); + setSiteId(site.id); + }, + [refreshSites], + ); + + // Native events: auth results, editor state, menu actions. + useEffect(() => { + const unlisten = [ + listen("auth://completed", (e) => void onSiteAdded(e.payload)), + listen("auth://failed", (e) => { + setWaitingForBrowser(false); + setAuthError(e.payload); + }), + listen("editor://state", (e) => setReport(e.payload)), + listen("menu://action", (e) => { + const { view, context } = state.current; + const inEditor = view.kind === "editor"; + const findType = (slug: string) => context?.postTypes.find((t) => t.slug === slug && t.canCreate); + switch (e.payload) { + case "new-post": + case "new-page": { + const t = findType(e.payload === "new-post" ? "post" : "page"); + if (!t) return; + void leaveEditor().then((ok) => { + if (!ok) return; + setPostType(t); + openEditor(null); + }); + return; + } + case "save": + if (inEditor) void api.editorCommand("save"); + return; + case "publish": + if (inEditor) void api.editorCommand("publish"); + return; + case "toggle-settings": + if (inEditor) void api.editorCommand("toggleSettings"); + return; + case "device-desktop": + case "device-tablet": + case "device-mobile": + if (inEditor) { + const d = e.payload.split("-")[1]; + void api.editorCommand(d === "desktop" ? "deviceDesktop" : d === "tablet" ? "deviceTablet" : "deviceMobile"); + } + return; + case "close-editor": + void leaveEditor(); + return; + case "add-site": + void startAddSite(); + return; + case "toggle-sidebar": + document.documentElement.classList.toggle("sidebar-hidden"); + window.dispatchEvent(new Event("resize")); + return; + } + }), + ]; + return () => unlisten.forEach((p) => void p.then((f) => f())); + }, [leaveEditor, onSiteAdded, openEditor, startAddSite]); + + return ( +

+ +
+ {sites.length === 0 ? ( + + ) : contextError ? ( +
+
{contextError}
+ +
+ ) : !context || !postType ? ( +
{context ? "Your role can't create or edit content on this site." : "Loading…"}
+ ) : view.kind === "editor" ? ( + void leaveEditor()} + /> + ) : ( + + )} +
+ {adding && ( + { + setAuthError(null); + setWaitingForBrowser(true); + }} + onAdded={onSiteAdded} + onCancel={() => setAdding(false)} + /> + )} +
+ ); +} + +function Welcome({ onAdd, platform }: { onAdd: () => void; platform: Platform }) { + return ( +
+
+ +
+

Welcome to WordPress Remote Editor

+

+ Write and publish on your WordPress sites with the full block editor, including blocks from your plugins. + Your sign-in is kept in the {platform === "macos" ? "macOS Keychain" : platform === "windows" ? "Windows Credential Manager" : "system keyring"}. +

+ +
+ ); +} diff --git a/src/components/AddSiteSheet.tsx b/src/components/AddSiteSheet.tsx new file mode 100644 index 0000000..07a9523 --- /dev/null +++ b/src/components/AddSiteSheet.tsx @@ -0,0 +1,202 @@ +import { useEffect, useRef, useState } from "react"; +import { api, errorMessage, type SiteDiscovery, type SiteSummary } from "../lib/api"; +import { hostOf } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + waitingForBrowser: boolean; + authError: string | null; + onStartedAuthorization: () => void; + onAdded: (site: SiteSummary) => void; + onCancel: () => void; +} + +type Step = "address" | "signin" | "manual"; + +export function AddSiteSheet({ waitingForBrowser, authError, onStartedAuthorization, onAdded, onCancel }: Props) { + const [step, setStep] = useState("address"); + const [url, setUrl] = useState(""); + const [allowLocal, setAllowLocal] = useState(false); + const [discovery, setDiscovery] = useState(null); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const firstField = useRef(null); + + useEffect(() => firstField.current?.focus(), [step]); + useEffect(() => setError(authError), [authError]); + useEffect(() => { + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onCancel(); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onCancel]); + + async function findSite(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const d = await api.discoverSite(url, allowLocal); + setDiscovery(d); + setStep(d.supportsApplicationPasswords ? "signin" : "manual"); + if (!d.supportsApplicationPasswords) { + setError("This site doesn't advertise Application Password sign-in. You can still try an application password created under Users → Profile."); + } + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + async function signInWithWordPress() { + setBusy(true); + setError(null); + try { + await api.beginAuthorization(discovery?.siteUrl ?? url, allowLocal); + onStartedAuthorization(); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + async function addManually(e: React.FormEvent) { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const site = await api.addSiteManual(discovery?.siteUrl ?? url, username, password, allowLocal); + setPassword(""); + onAdded(site); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + return ( +
e.target === e.currentTarget && onCancel()}> +
+

Add a WordPress Site

+ + {step === "address" && ( +
+

Enter the address of a site you can sign in to.

+ + + {error &&

{error}

} +
+ + +
+
+ )} + + {step === "signin" && discovery && ( +
+
+ +
+
{discovery.name}
+
{hostOf(discovery.siteUrl)}
+
+
+ {waitingForBrowser ? ( +

+ Approve WordPress Remote Editor in the sign-in window. This sheet updates when you're done. +

+ ) : ( +

+ You'll sign in on your site and approve this app. WordPress creates an Application Password just for this app, which you can revoke from your profile at any time. +

+ )} + {error &&

{error}

} +
+ +
+ + +
+
+
+ )} + + {step === "manual" && ( +
+

+ Create an Application Password in WordPress under Users → Profile → Application Passwords, then paste it here. It's stored only in your system keychain. +

+ + + {error &&

{error}

} +
+ +
+ + +
+
+
+ )} +
+
+ ); +} diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx new file mode 100644 index 0000000..797812e --- /dev/null +++ b/src/components/EditorView.tsx @@ -0,0 +1,145 @@ +import { useLayoutEffect, useRef, useState } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { api, errorMessage, type Bounds, type EditorReport, type PostTypeAccess, type SiteContext } from "../lib/api"; +import { statusLabel } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + context: SiteContext; + postType: PostTypeAccess; + postId: number | null; + report: EditorReport | null; + onBack: () => void; +} + +type Device = "Desktop" | "Tablet" | "Mobile"; + +function measure(el: HTMLElement): Bounds { + const r = el.getBoundingClientRect(); + return { x: r.left, y: r.top, width: r.width, height: r.height }; +} + +/** + * Native toolbar plus a placeholder region. The Rust core places the site's + * block editor (a separate, isolated webview) exactly over the placeholder. + */ +export function EditorView({ context, postType, postId, report, onBack }: Props) { + const host = useRef(null); + const [error, setError] = useState(null); + const [device, setDevice] = useState("Desktop"); + + useLayoutEffect(() => { + const el = host.current; + if (!el) return; + let cancelled = false; + setError(null); + api.openEditor(context.site.id, postType.slug, postId, measure(el)).catch((e) => { + if (!cancelled) setError(errorMessage(e)); + }); + let frame = 0; + const sync = () => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => host.current && api.setEditorBounds(measure(host.current)).catch(() => {})); + }; + const ro = new ResizeObserver(sync); + ro.observe(el); + window.addEventListener("resize", sync); + return () => { + cancelled = true; + ro.disconnect(); + window.removeEventListener("resize", sync); + cancelAnimationFrame(frame); + }; + }, [context.site.id, postType.slug, postId]); + + const isEditor = report?.kind === "editor"; + const status = report?.status ?? ""; + const canPublish = report?.canPublish ?? postType.canPublish; + const published = status === "publish" || status === "future" || status === "private"; + const primaryLabel = published ? "Update" : canPublish ? "Publish…" : "Submit for Review"; + const title = isEditor ? report?.title || "Untitled" : postId ? "Loading…" : `New ${postType.singularName}`; + + const cmd = (c: Parameters[0]) => api.editorCommand(c).catch((e) => setError(errorMessage(e))); + const switchDevice = (d: Device) => { + setDevice(d); + void cmd(d === "Desktop" ? "deviceDesktop" : d === "Tablet" ? "deviceTablet" : "deviceMobile"); + }; + + return ( +
+
+ +
+ {title} + {isEditor && ( + + {report?.saving ? "Saving…" : report?.dirty ? "Edited" : statusLabel(status)} + + )} +
+
+ {isEditor && ( + <> +
+ + +
+
+ {(["Desktop", "Tablet", "Mobile"] as Device[]).map((d) => ( + + ))} +
+ {report?.link && status === "publish" && ( + + )} + + {!published && ( + + )} + + + )} +
+ + {report?.kind === "login" && ( +
+ Sign in to {context.site.name} once below to open the editor. + {!context.site.bridgeAvailable && " Install the Remote Editor Bridge plugin on the site to skip this step."} +
+ )} + {error && ( +
+ {error} +
+ )} + +
+
Loading the block editor…
+
+
+ ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx new file mode 100644 index 0000000..06488e2 --- /dev/null +++ b/src/components/Icon.tsx @@ -0,0 +1,42 @@ +// Small inline icon set (stroke icons, 20px grid) so nothing loads remotely. +const paths: Record = { + plus: "M10 4v12M4 10h12", + back: "M12.5 4.5 7 10l5.5 5.5", + search: "M9 15a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm4.5-1.5L17 17", + post: "M5 3.5h10v13H5zM7.5 7h5M7.5 10h5M7.5 13h3", + page: "M6 2.5h5.5L15 6v11.5H6zM11.5 2.5V6H15", + type: "M4 5h12M4 10h12M4 15h8", + undo: "M7 8H13a3.5 3.5 0 0 1 0 7H9M7 8l3-3M7 8l3 3", + redo: "M13 8H7a3.5 3.5 0 0 0 0 7h4M13 8l-3-3M13 8l-3 3", + settings: "M3.5 4.5h13v11h-13zM12 4.5v11", + desktop: "M3 4h14v9H3zM7.5 16.5h5M10 13v3.5", + tablet: "M5.5 2.5h9v15h-9zM9 15h2", + mobile: "M7 2.5h6v15H7zM9.5 15h1", + external: "M11 3.5h5.5V9M16.5 3.5 9 11M14 12v4.5H3.5V6H8", + trash: "M4 6h12M8 6V4h4v2M5.5 6l.8 10.5h7.4L14.5 6", + more: "M5 10h.01M10 10h.01M15 10h.01", + globe: "M10 17a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM3 10h14M10 3c2 2 2.8 4.5 2.8 7S12 15 10 17c-2-2-2.8-4.5-2.8-7S8 5 10 3Z", + lock: "M5.5 9h9v7.5h-9zM7.5 9V6.5a2.5 2.5 0 0 1 5 0V9", + check: "M4.5 10.5 8 14l7.5-8", + refresh: "M15.5 8A6 6 0 0 0 4.6 6.5M4.5 3.5v3h3M4.5 12a6 6 0 0 0 10.9 1.5M15.5 16.5v-3h-3", +}; + +export function Icon({ name, size = 18, title }: { name: keyof typeof paths | string; size?: number; title?: string }) { + return ( + + {title ? {title} : null} + + + ); +} diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx new file mode 100644 index 0000000..dc754f8 --- /dev/null +++ b/src/components/PostList.tsx @@ -0,0 +1,207 @@ +import { useCallback, useEffect, useState } from "react"; +import { ask } from "@tauri-apps/plugin-dialog"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import { api, errorMessage, type PostSummary, type PostTypeAccess, type SiteContext, type StatusFilter } from "../lib/api"; +import { relativeTime, statusLabel } from "../lib/format"; +import { Icon } from "./Icon"; + +interface Props { + context: SiteContext; + postType: PostTypeAccess; + onOpen: (postId: number | null) => void; +} + +const FILTERS: { id: StatusFilter; label: string }[] = [ + { id: "any", label: "All" }, + { id: "draft", label: "Drafts" }, + { id: "pending", label: "Pending" }, + { id: "future", label: "Scheduled" }, + { id: "publish", label: "Published" }, +]; + +export function PostList({ context, postType, onOpen }: Props) { + const [filter, setFilter] = useState("any"); + const [mineOnly, setMineOnly] = useState(!postType.canEditOthers); + const [search, setSearch] = useState(""); + const [query, setQuery] = useState(""); + const [items, setItems] = useState([]); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [selected, setSelected] = useState(null); + + useEffect(() => setMineOnly(!postType.canEditOthers), [postType]); + + // Debounce search input. + useEffect(() => { + const t = window.setTimeout(() => setQuery(search), 300); + return () => window.clearTimeout(t); + }, [search]); + + const load = useCallback( + async (nextPage: number) => { + setLoading(true); + setError(null); + try { + const res = await api.listPosts({ + siteId: context.site.id, + restBase: postType.restBase, + status: filter, + search: query, + page: nextPage, + mineOnly, + canEditOthers: postType.canEditOthers, + userId: context.userId, + }); + setItems((prev) => (nextPage === 1 ? res.items : [...prev, ...res.items])); + setPage(res.page); + setTotalPages(res.totalPages); + } catch (e) { + setError(errorMessage(e)); + if (nextPage === 1) setItems([]); + } finally { + setLoading(false); + } + }, + [context.site.id, context.userId, postType, filter, query, mineOnly], + ); + + useEffect(() => { + void load(1); + }, [load]); + + async function trash(p: PostSummary) { + const ok = await ask(`Move “${p.title || "(no title)"}” to the Trash? You can restore it from WordPress.`, { + title: "Move to Trash", + kind: "warning", + okLabel: "Move to Trash", + cancelLabel: "Cancel", + }); + if (!ok) return; + try { + await api.trashPost(context.site.id, postType.restBase, p.id); + setItems((prev) => prev.filter((x) => x.id !== p.id)); + } catch (e) { + setError(errorMessage(e)); + } + } + + return ( +
+
+

{postType.name}

+
+ {FILTERS.map((f) => ( + + ))} +
+
+ + + {postType.canCreate && ( + + )} +
+ + {postType.canEditOthers && ( +
+ +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ {items.map((p) => ( +
setSelected(p.id)} + onDoubleClick={() => p.canEdit && onOpen(p.id)} + onKeyDown={(e) => { + if (e.key === "Enter" && p.canEdit) onOpen(p.id); + }} + > +
+
{p.title || (no title)}
+
+ {statusLabel(p.status)} + {relativeTime(p.modified)} +
+
+
+ {p.link && p.status === "publish" && ( + + )} + {p.canEdit && postType.canDelete && ( + + )} + {p.canEdit && ( + + )} +
+
+ ))} + + {!loading && items.length === 0 && !error && ( +
+ +

No {postType.name.toLowerCase()} here yet.

+ {postType.canCreate && ( + + )} +
+ )} + + {loading &&
Loading…
} + {!loading && page < totalPages && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..eaf67dc --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,97 @@ +import type { PostTypeAccess, SiteContext, SiteSummary } from "../lib/api"; +import { roleLabel } from "../lib/format"; +import { Icon } from "./Icon"; +import { SiteAvatar, UserAvatar } from "./SiteAvatar"; + +interface Props { + sites: SiteSummary[]; + selectedSiteId: string | null; + context: SiteContext | null; + selectedType: string | null; + onSelectSite: (id: string) => void; + onSelectType: (t: PostTypeAccess) => void; + onAddSite: () => void; + onRemoveSite: (site: SiteSummary) => void; +} + +export function Sidebar(props: Props) { + const { sites, selectedSiteId, context, selectedType } = props; + return ( + + ); +} diff --git a/src/components/SiteAvatar.tsx b/src/components/SiteAvatar.tsx new file mode 100644 index 0000000..6b99ed5 --- /dev/null +++ b/src/components/SiteAvatar.tsx @@ -0,0 +1,40 @@ +import { useState } from "react"; +import type { SiteSummary } from "../lib/api"; + +/** Site initial in a tinted tile; deterministic colour per site. */ +export function SiteAvatar({ site, size = 22 }: { site: Pick; size?: number }) { + let hash = 0; + for (const ch of site.id) hash = (hash * 31 + ch.charCodeAt(0)) | 0; + const hue = Math.abs(hash) % 360; + return ( + + {(site.name.trim()[0] ?? "W").toUpperCase()} + + ); +} + +export function UserAvatar({ url, name, size = 24 }: { url: string | null; name: string; size?: number }) { + const [failed, setFailed] = useState(false); + if (!url || failed) { + return ( + + {(name.trim()[0] ?? "?").toUpperCase()} + + ); + } + return ( + setFailed(true)} + /> + ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..adfeb2d --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,127 @@ +// Typed wrappers around the Rust core's IPC commands. The shell never talks +// to WordPress directly and never handles passwords after submitting them. +import { invoke } from "@tauri-apps/api/core"; + +export interface SiteSummary { + id: string; + name: string; + url: string; + username: string; + displayName: string; + avatarUrl: string | null; + bridgeAvailable: boolean; +} + +export interface SiteDiscovery { + siteUrl: string; + adminUrl: string; + apiRoot: string; + name: string; + description: string; + supportsApplicationPasswords: boolean; + bridgeAvailable: boolean; +} + +export interface PostTypeAccess { + slug: string; + restBase: string; + name: string; + singularName: string; + hierarchical: boolean; + canCreate: boolean; + canPublish: boolean; + canEditOthers: boolean; + canDelete: boolean; +} + +export interface SiteContext { + site: SiteSummary; + userId: number; + roles: string[]; + postTypes: PostTypeAccess[]; +} + +export interface PostSummary { + id: number; + title: string; + status: string; + modified: string | null; + author: number | null; + link: string | null; + canEdit: boolean; +} + +export interface PostPage { + items: PostSummary[]; + total: number; + totalPages: number; + page: number; +} + +export interface EditorReport { + kind: "editor" | "login" | "page"; + postId?: number | null; + postType?: string | null; + title?: string | null; + status?: string | null; + link?: string | null; + dirty?: boolean | null; + saving?: boolean | null; + canPublish?: boolean | null; + path?: string | null; +} + +export interface Bounds { + x: number; + y: number; + width: number; + height: number; +} + +export type EditorCommand = + | "save" + | "publish" + | "undo" + | "redo" + | "toggleSettings" + | "deviceDesktop" + | "deviceTablet" + | "deviceMobile"; + +export type StatusFilter = "any" | "draft" | "pending" | "future" | "publish" | "private" | "trash"; + +export const api = { + discoverSite: (url: string, allowInsecureLocal = false) => + invoke("discover_site", { url, allowInsecureLocal }), + beginAuthorization: (url: string, allowInsecureLocal = false) => + invoke("begin_authorization", { url, allowInsecureLocal }), + addSiteManual: (url: string, username: string, appPassword: string, allowInsecureLocal = false) => + invoke("add_site_manual", { url, username, appPassword, allowInsecureLocal }), + listSites: () => invoke("list_sites"), + removeSite: (siteId: string) => invoke("remove_site", { siteId }), + getSiteContext: (siteId: string) => invoke("get_site_context", { siteId }), + listPosts: (args: { + siteId: string; + restBase: string; + status: StatusFilter; + search: string; + page: number; + mineOnly: boolean; + canEditOthers: boolean; + userId: number; + }) => invoke("list_posts", { query: args }), + trashPost: (siteId: string, restBase: string, postId: number) => + invoke("trash_post", { siteId, restBase, postId }), + openEditor: (siteId: string, postType: string, postId: number | null, bounds: Bounds) => + invoke("open_editor", { siteId, postType, postId, bounds }), + closeEditor: () => invoke("close_editor"), + setEditorBounds: (bounds: Bounds) => invoke("editor_set_bounds", { bounds }), + editorCommand: (command: EditorCommand) => invoke("editor_command", { command }), +}; + +/** Errors from the core arrive as plain user-facing strings. */ +export function errorMessage(e: unknown): string { + if (typeof e === "string") return e; + if (e instanceof Error) return e.message; + return "Something went wrong."; +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..57d7658 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { hostOf, relativeTime, roleLabel, statusLabel } from "./format"; + +describe("format helpers", () => { + it("labels WordPress statuses", () => { + expect(statusLabel("publish")).toBe("Published"); + expect(statusLabel("future")).toBe("Scheduled"); + expect(statusLabel("custom")).toBe("Custom"); + expect(statusLabel(null)).toBe(""); + }); + + it("formats relative times", () => { + const now = new Date("2026-10-13T12:00:00"); + expect(relativeTime("2026-10-13T11:58:00", now)).toMatch(/2 minutes ago/); + expect(relativeTime("not a date", now)).toBe(""); + expect(relativeTime(null, now)).toBe(""); + }); + + it("labels roles", () => { + expect(roleLabel(["editor"])).toBe("Editor"); + expect(roleLabel(["shop_manager"])).toBe("shop manager"); + }); + + it("extracts hosts", () => { + expect(hostOf("https://example.com/blog/")).toBe("example.com"); + }); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..082effa --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,49 @@ +const STATUS_LABELS: Record = { + publish: "Published", + draft: "Draft", + pending: "Pending Review", + future: "Scheduled", + private: "Private", + trash: "Trash", + "auto-draft": "Draft", +}; + +export function statusLabel(status: string | null | undefined): string { + if (!status) return ""; + return STATUS_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1); +} + +/** WordPress returns `modified` as local site time without a zone. */ +export function relativeTime(iso: string | null, now: Date = new Date()): string { + if (!iso) return ""; + const then = new Date(iso); + if (Number.isNaN(then.getTime())) return ""; + const seconds = Math.round((now.getTime() - then.getTime()) / 1000); + const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); + const abs = Math.abs(seconds); + if (abs < 60) return rtf.format(-seconds, "second"); + if (abs < 3600) return rtf.format(-Math.round(seconds / 60), "minute"); + if (abs < 86400) return rtf.format(-Math.round(seconds / 3600), "hour"); + if (abs < 86400 * 7) return rtf.format(-Math.round(seconds / 86400), "day"); + return then.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); +} + +/** Turns WordPress role slugs into readable names. */ +export function roleLabel(roles: string[]): string { + const known: Record = { + administrator: "Administrator", + editor: "Editor", + author: "Author", + contributor: "Contributor", + subscriber: "Subscriber", + }; + return roles.map((r) => known[r] ?? r.replace(/[_-]+/g, " ")).join(", "); +} + +export function hostOf(url: string): string { + try { + return new URL(url).host; + } catch { + return url; + } +} diff --git a/src/lib/platform.ts b/src/lib/platform.ts new file mode 100644 index 0000000..8ef72af --- /dev/null +++ b/src/lib/platform.ts @@ -0,0 +1,20 @@ +export type Platform = "macos" | "windows" | "linux"; + +/** Resolves the host OS so the shell can adopt that platform's conventions. */ +export async function detectPlatform(): Promise { + try { + const os = await import("@tauri-apps/plugin-os"); + const p = os.platform(); + if (p === "macos" || p === "windows") return p; + return "linux"; + } catch { + const ua = navigator.userAgent; + if (/Mac/.test(ua)) return "macos"; + if (/Windows/.test(ua)) return "windows"; + return "linux"; + } +} + +export function modKey(platform: Platform): string { + return platform === "macos" ? "⌘" : "Ctrl+"; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..28e81d0 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import { detectPlatform } from "./lib/platform"; +import "./styles/app.css"; + +void detectPlatform().then((platform) => { + document.documentElement.dataset.platform = platform; + createRoot(document.getElementById("root")!).render( + + + , + ); +}); + +// Native apps don't show a browser context menu on chrome surfaces. +window.addEventListener("contextmenu", (e) => { + const t = e.target as HTMLElement | null; + if (!t?.closest("input, textarea, [contenteditable='true']")) e.preventDefault(); +}); diff --git a/src/styles/app.css b/src/styles/app.css new file mode 100644 index 0000000..39525a0 --- /dev/null +++ b/src/styles/app.css @@ -0,0 +1,862 @@ +/* + * Platform-adaptive shell styles. `data-platform` on selects macOS, + * Windows (Fluent) or Linux (libadwaita-like) metrics; the OS light/dark + * setting is followed through prefers-color-scheme. + */ + +/* ---- Base tokens (Linux / GNOME defaults) ------------------------------- */ +:root { + --font: "Cantarell", "Inter", "Noto Sans", "Ubuntu", system-ui, sans-serif; + --font-size: 14px; + --accent: #3584e4; + --accent-text: #ffffff; + --text: #1e1e1e; + --text-muted: rgba(0, 0, 0, 0.55); + --window-bg: #fafafa; + --sidebar-bg: #ebebed; + --content-bg: #ffffff; + --toolbar-bg: #ffffff; + --border: rgba(0, 0, 0, 0.1); + --hover: rgba(0, 0, 0, 0.05); + --selected: rgba(0, 0, 0, 0.08); + --selected-text: var(--text); + --control-bg: rgba(0, 0, 0, 0.06); + --control-bg-hover: rgba(0, 0, 0, 0.1); + --field-bg: #ffffff; + --danger: #c01c28; + --success: #26a269; + --warning: #c64600; + --radius: 8px; + --radius-small: 6px; + --control-height: 34px; + --toolbar-height: 47px; + --sidebar-width: 248px; + --titlebar-inset: 0px; + --focus-ring: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent); + --shadow-sheet: 0 12px 40px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.06); + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #ffffff; + --text-muted: rgba(255, 255, 255, 0.6); + --window-bg: #242424; + --sidebar-bg: #2e2e32; + --content-bg: #1e1e1e; + --toolbar-bg: #2a2a2a; + --border: rgba(255, 255, 255, 0.1); + --hover: rgba(255, 255, 255, 0.06); + --selected: rgba(255, 255, 255, 0.1); + --control-bg: rgba(255, 255, 255, 0.1); + --control-bg-hover: rgba(255, 255, 255, 0.15); + --field-bg: rgba(255, 255, 255, 0.08); + --danger: #ff7b63; + --success: #8ff0a4; + --warning: #ffa348; + } +} + +/* ---- macOS ------------------------------------------------------------- */ +:root[data-platform="macos"] { + --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; + --font-size: 13px; + --accent: AccentColor; + --accent-text: AccentColorText; + --window-bg: transparent; + --sidebar-bg: transparent; /* native sidebar vibrancy shows through */ + --content-bg: #ffffff; + --toolbar-bg: #ffffff; + --selected: rgba(0, 0, 0, 0.1); + --control-bg: #ffffff; + --control-bg-hover: #f5f5f5; + --radius: 6px; + --radius-small: 5px; + --control-height: 24px; + --toolbar-height: 52px; + --sidebar-width: 232px; + --titlebar-inset: 40px; /* room for the traffic lights */ + --shadow-sheet: 0 20px 60px rgba(0, 0, 0, 0.3), 0 0 0 0.5px rgba(0, 0, 0, 0.2); +} +@supports not (color: AccentColor) { + :root[data-platform="macos"] { + --accent: #007aff; + --accent-text: #ffffff; + } +} +@media (prefers-color-scheme: dark) { + :root[data-platform="macos"] { + --content-bg: #1e1e1e; + --toolbar-bg: #2b2b2b; + --selected: rgba(255, 255, 255, 0.12); + --control-bg: rgba(255, 255, 255, 0.14); + --control-bg-hover: rgba(255, 255, 255, 0.2); + } +} + +/* ---- Windows 11 (Fluent) ----------------------------------------------- */ +:root[data-platform="windows"] { + --font: "Segoe UI Variable Text", "Segoe UI Variable", "Segoe UI", system-ui, sans-serif; + --font-size: 14px; + --accent: AccentColor; + --accent-text: AccentColorText; + --window-bg: transparent; + --sidebar-bg: transparent; /* Mica shows through */ + --content-bg: rgba(255, 255, 255, 0.7); + --toolbar-bg: transparent; + --hover: rgba(0, 0, 0, 0.037); + --selected: rgba(0, 0, 0, 0.06); + --control-bg: rgba(255, 255, 255, 0.7); + --control-bg-hover: rgba(249, 249, 249, 0.5); + --radius: 8px; + --radius-small: 4px; + --control-height: 32px; + --toolbar-height: 52px; + --sidebar-width: 260px; +} +@supports not (color: AccentColor) { + :root[data-platform="windows"] { + --accent: #005fb8; + --accent-text: #ffffff; + } +} +@media (prefers-color-scheme: dark) { + :root[data-platform="windows"] { + --content-bg: rgba(58, 58, 58, 0.3); + --hover: rgba(255, 255, 255, 0.06); + --selected: rgba(255, 255, 255, 0.08); + --control-bg: rgba(255, 255, 255, 0.06); + --control-bg-hover: rgba(255, 255, 255, 0.08); + } +} + +/* ---- Reset ------------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; +} +html, +body, +#root { + height: 100%; + margin: 0; + overflow: hidden; +} +body { + font-family: var(--font); + font-size: var(--font-size); + color: var(--text); + background: var(--window-bg); + -webkit-font-smoothing: antialiased; + user-select: none; + -webkit-user-select: none; + cursor: default; +} +input, +textarea, +.row-title { + user-select: text; + -webkit-user-select: text; +} +button, +input { + font: inherit; + color: inherit; +} +:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +h1, +h2 { + margin: 0; + font-weight: 600; +} +.muted { + color: var(--text-muted); +} + +/* ---- Layout ------------------------------------------------------------ */ +.app { + display: flex; + height: 100%; +} +.main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + background: var(--content-bg); +} +:root[data-platform="windows"] .main { + border-top-left-radius: 8px; + border-left: 1px solid var(--border); + border-top: 1px solid var(--border); + margin-top: 0; +} +.sidebar-hidden .sidebar { + display: none; +} +.centered { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + align-items: center; + justify-content: center; + padding: 24px; +} + +/* ---- Sidebar ----------------------------------------------------------- */ +.sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + display: flex; + flex-direction: column; + background: var(--sidebar-bg); + border-right: 1px solid var(--border); +} +:root[data-platform="macos"] .sidebar, +:root[data-platform="windows"] .sidebar { + border-right: none; +} +.sidebar-titlebar { + height: var(--titlebar-inset); + flex-shrink: 0; +} +.sidebar-scroll { + flex: 1; + overflow-y: auto; + padding: 8px 10px; +} +.sidebar-section + .sidebar-section { + margin-top: 14px; +} +.sidebar-section ul { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar-heading { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + padding: 4px 8px; +} +:root[data-platform="linux"] .sidebar-heading { + font-size: 12px; + font-weight: 700; +} +.sidebar-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + min-height: 28px; + padding: 4px 8px; + border: none; + background: transparent; + border-radius: var(--radius-small); + text-align: left; +} +:root[data-platform="windows"] .sidebar-item { + min-height: 36px; + padding: 0 12px; + position: relative; +} +:root[data-platform="linux"] .sidebar-item { + min-height: 36px; + padding: 0 10px; +} +.sidebar-item:hover { + background: var(--hover); +} +.sidebar-item.selected { + background: var(--selected); + color: var(--selected-text); +} +:root[data-platform="windows"] .sidebar-item.selected::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 3px; + height: 16px; + margin-top: -8px; + border-radius: 2px; + background: var(--accent); +} +.sidebar-item .label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar-item.subtle { + color: var(--text-muted); +} +.sidebar-item svg { + color: var(--accent); + flex-shrink: 0; +} +.icon-tile { + display: inline-flex; + width: 22px; + height: 22px; + align-items: center; + justify-content: center; + border-radius: 5px; + background: var(--control-bg); +} +.badge { + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + background: var(--control-bg); + color: var(--text-muted); +} +.site-avatar, +.user-avatar.fallback { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 5px; + color: #fff; + font-weight: 600; + flex-shrink: 0; +} +.user-avatar { + border-radius: 50%; + flex-shrink: 0; +} +.user-avatar.fallback { + border-radius: 50%; + background: var(--control-bg-hover); + color: var(--text); +} +.sidebar-footer { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-top: 1px solid var(--border); +} +.sidebar-footer .who { + flex: 1; + min-width: 0; +} +.sidebar-footer .name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar-footer .role { + font-size: 11px; + color: var(--text-muted); +} + +/* ---- Toolbar ----------------------------------------------------------- */ +.content { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +.toolbar { + display: flex; + align-items: center; + gap: 8px; + height: var(--toolbar-height); + padding: 0 14px; + background: var(--toolbar-bg); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.toolbar-title { + font-size: 15px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 40vw; +} +:root[data-platform="macos"] .toolbar-title { + font-size: 13px; + font-weight: 700; +} +:root[data-platform="windows"] .toolbar-title { + font-size: 20px; + font-weight: 600; +} +.toolbar-title-group { + display: flex; + flex-direction: column; + min-width: 0; + line-height: 1.2; +} +.toolbar-subtitle { + font-size: 11px; + color: var(--text-muted); +} +.toolbar-spacer { + flex: 1; + align-self: stretch; +} +.subbar { + display: flex; + align-items: center; + gap: 12px; + padding: 6px 16px; + border-bottom: 1px solid var(--border); +} + +/* ---- Controls ---------------------------------------------------------- */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: var(--control-height); + padding: 0 14px; + border-radius: var(--radius-small); + border: 1px solid var(--border); + background: var(--control-bg); + white-space: nowrap; +} +.button:hover:not(:disabled) { + background: var(--control-bg-hover); +} +.button:disabled { + opacity: 0.5; +} +.button.primary { + background: var(--accent); + color: var(--accent-text); + border-color: transparent; +} +.button.primary:hover:not(:disabled) { + filter: brightness(1.08); + background: var(--accent); +} +.button.borderless { + border-color: transparent; + background: transparent; + padding: 0 8px; +} +.button.borderless:hover { + background: var(--hover); +} +.button.small { + height: calc(var(--control-height) - 4px); + padding: 0 10px; +} +.button.large { + height: 40px; + padding: 0 22px; + font-size: 15px; +} +:root[data-platform="macos"] .button { + padding: 0 10px; + box-shadow: 0 0.5px 1px rgba(0, 0, 0, 0.12); + border-color: rgba(0, 0, 0, 0.08); +} +:root[data-platform="macos"] .button.large { + height: 32px; + font-size: 13px; +} +:root[data-platform="linux"] .button { + border: none; + font-weight: 600; +} +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--control-height); + height: var(--control-height); + min-width: 28px; + min-height: 28px; + border: none; + border-radius: var(--radius-small); + background: transparent; + color: var(--text-muted); +} +.icon-button:hover { + background: var(--hover); + color: var(--text); +} +.icon-button.danger:hover { + color: var(--danger); +} +.button-group { + display: inline-flex; +} +.link-button { + border: none; + background: none; + color: var(--accent); + padding: 0; +} +.link-button:hover { + text-decoration: underline; +} +.segmented { + display: inline-flex; + padding: 2px; + gap: 2px; + border-radius: var(--radius); + background: var(--control-bg); + border: 1px solid var(--border); +} +.segmented button { + border: none; + background: transparent; + padding: 0 10px; + height: calc(var(--control-height) - 6px); + min-height: 20px; + border-radius: calc(var(--radius) - 2px); + color: var(--text-muted); + display: inline-flex; + align-items: center; +} +.segmented button.active { + background: var(--content-bg); + color: var(--text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); +} +:root[data-platform="macos"] .segmented button.active { + background: #fff; +} +@media (prefers-color-scheme: dark) { + .segmented button.active, + :root[data-platform="macos"] .segmented button.active { + background: rgba(255, 255, 255, 0.18); + } +} +.segmented.icons button { + padding: 0 6px; +} +.search-field { + display: inline-flex; + align-items: center; + gap: 6px; + height: var(--control-height); + min-height: 26px; + padding: 0 8px; + border-radius: var(--radius-small); + background: var(--field-bg); + border: 1px solid var(--border); + color: var(--text-muted); + width: 200px; +} +.search-field input { + border: none; + outline: none; + background: transparent; + width: 100%; + color: var(--text); +} +.field { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 12px; +} +.field span { + font-weight: 500; +} +.field input { + height: 32px; + padding: 0 10px; + border-radius: var(--radius-small); + border: 1px solid var(--border); + background: var(--field-bg); +} +:root[data-platform="windows"] .field input { + border-bottom: 2px solid var(--text-muted); +} +:root[data-platform="windows"] .field input:focus { + border-bottom-color: var(--accent); + box-shadow: none; +} +.checkbox { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 4px 0 12px; + color: var(--text-muted); +} +.checkbox.small { + margin: 0; + align-items: center; +} +.checkbox input { + accent-color: var(--accent); + margin: 2px 0 0; +} + +/* ---- Banners ----------------------------------------------------------- */ +.banner { + padding: 8px 16px; + font-size: 13px; + border-bottom: 1px solid var(--border); +} +.banner.error { + color: var(--danger); + background: color-mix(in srgb, var(--danger) 9%, transparent); +} +.banner.info { + background: color-mix(in srgb, var(--accent) 10%, transparent); +} +.centered .banner { + border: none; + border-radius: var(--radius); +} + +/* ---- Post list --------------------------------------------------------- */ +.list { + flex: 1; + overflow-y: auto; + padding: 6px 8px 16px; +} +.row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: var(--radius); + outline: none; +} +.row + .row { + margin-top: 1px; +} +.row:hover { + background: var(--hover); +} +.row.selected { + background: var(--selected); +} +:root[data-platform="macos"] .row.selected { + background: var(--accent); + color: var(--accent-text); +} +:root[data-platform="macos"] .row.selected .muted, +:root[data-platform="macos"] .row.selected .status, +:root[data-platform="macos"] .row.selected .icon-button { + color: inherit; + opacity: 0.85; +} +.row:focus-visible { + box-shadow: var(--focus-ring); +} +.row.readonly .row-title { + color: var(--text-muted); +} +.row-main { + flex: 1; + min-width: 0; +} +.row-title { + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.row-meta { + display: flex; + gap: 10px; + font-size: 12px; + margin-top: 2px; +} +.row-actions { + display: flex; + align-items: center; + gap: 4px; + opacity: 0; +} +.row:hover .row-actions, +.row.selected .row-actions, +.row:focus-within .row-actions { + opacity: 1; +} +.status { + font-weight: 500; +} +.status-publish { + color: var(--success); +} +.status-draft { + color: var(--text-muted); +} +.status-pending { + color: var(--warning); +} +.status-future { + color: var(--accent); +} +.empty, +.loading, +.load-more { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 48px 16px; + color: var(--text-muted); +} +.load-more { + padding: 16px; +} + +/* ---- Editor ------------------------------------------------------------ */ +.editor-host { + position: relative; + flex: 1; + min-height: 0; + background: var(--content-bg); +} +.editor-loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); +} + +/* ---- Welcome ----------------------------------------------------------- */ +.welcome { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 14px; + padding: 32px; +} +.welcome p { + max-width: 460px; + color: var(--text-muted); + line-height: 1.5; + margin: 0 0 8px; +} +.welcome-icon { + width: 84px; + height: 84px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + background: linear-gradient(180deg, #466cf0, #1d35b8); + box-shadow: 0 8px 24px rgba(29, 53, 184, 0.35); +} +.welcome h1 { + font-size: 22px; +} + +/* ---- Sheet (modal) ----------------------------------------------------- */ +.sheet-backdrop { + position: fixed; + inset: 0; + display: flex; + align-items: flex-start; + justify-content: center; + background: rgba(0, 0, 0, 0.18); + animation: fade 120ms ease-out; +} +:root[data-platform="windows"] .sheet-backdrop, +:root[data-platform="linux"] .sheet-backdrop { + align-items: center; + background: rgba(0, 0, 0, 0.32); +} +.sheet { + width: min(480px, calc(100vw - 32px)); + margin-top: var(--titlebar-inset); + padding: 22px 24px 18px; + border-radius: 12px; + background: var(--content-bg); + box-shadow: var(--shadow-sheet); + animation: drop 160ms ease-out; +} +:root[data-platform="macos"] .sheet { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +:root[data-platform="windows"] .sheet, +:root[data-platform="linux"] .sheet { + margin-top: 0; +} +:root[data-platform="windows"] .sheet { + background: color-mix(in srgb, var(--content-bg) 100%, #f3f3f3); + border-radius: 8px; +} +.sheet h2 { + font-size: 17px; + margin-bottom: 8px; +} +.sheet-intro { + color: var(--text-muted); + line-height: 1.45; + margin: 0 0 14px; +} +.sheet-intro.waiting { + color: var(--text); +} +.sheet-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 8px; +} +.sheet-actions.spread { + justify-content: space-between; + align-items: center; +} +.form-error { + color: var(--danger); + margin: 0 0 8px; + line-height: 1.4; +} +.site-card { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + margin-bottom: 14px; + border-radius: var(--radius); + background: var(--control-bg); + border: 1px solid var(--border); +} +.site-card svg { + color: var(--accent); +} +.site-card-name { + font-weight: 600; +} +.site-card-host { + color: var(--text-muted); + font-size: 12px; +} + +@keyframes fade { + from { + opacity: 0; + } +} +@keyframes drop { + from { + transform: translateY(-12px); + opacity: 0; + } +} +@media (prefers-reduced-motion: reduce) { + .sheet, + .sheet-backdrop { + animation: none; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6d77e81 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..f8b47ba --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +const host = process.env.TAURI_DEV_HOST; + +export default defineConfig({ + plugins: [react()], + clearScreen: false, + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host ? { protocol: "ws", host, port: 1421 } : undefined, + watch: { ignored: ["**/src-tauri/**"] }, + }, + build: { + target: ["es2022", "safari16"], + sourcemap: false, + }, + test: { + environment: "jsdom", + }, +}); From 4dd8dae3873af3933eef469e7d6cbcaa0ea8da84 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 04:09:57 +0000 Subject: [PATCH 2/4] Fix sign-in window reuse, name app passwords per device, lay out editor on Linux Found by running the installed app against a real WordPress site: - reopening sign-in failed because the closing window's label was still taken - re-adding a site failed because WordPress rejects duplicate password names - on Linux the editor webview was stacked under the shell instead of placed below the toolbar; the main window now hosts the shell and editor as sibling webviews and the editor uses a full-width focus layout everywhere Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_013wWY5SvB2w46Qw92qtHG5e --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 3 ++ src-tauri/src/auth.rs | 68 +++++++++++++++++++++++++++---- src-tauri/src/editor.rs | 42 +++++++++++++++++++ src-tauri/src/lib.rs | 30 +++++++++++++- src-tauri/tauri.conf.json | 5 +-- src-tauri/tauri.macos.conf.json | 3 +- src-tauri/tauri.windows.conf.json | 7 +++- src/App.tsx | 2 +- src/styles/app.css | 9 +++- 10 files changed, 154 insertions(+), 16 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2bcfe3d..f1ea9ea 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5520,6 +5520,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "chrono", + "gtk", "keyring", "log", "rand 0.8.8", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f35c119..6ec9fb9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,3 +43,6 @@ lto = true opt-level = "s" panic = "abort" strip = true + +[target.'cfg(target_os = "linux")'.dependencies] +gtk = "0.18" diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 7cb8226..2db19a2 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -42,10 +42,27 @@ fn constant_time_eq(a: &str, b: &str) -> bool { a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 } -pub fn authorize_url(endpoint: &Url, state: &str) -> Url { +/// WordPress rejects a second Application Password with the same name, so +/// name each one after this device and the time it was created. That also +/// tells the user which computer a password belongs to on their profile page. +pub fn password_name(device: &str, now: chrono::DateTime) -> String { + let device: String = device + .chars() + .filter(|c| !c.is_control()) + .take(40) + .collect(); + let stamp = now.format("%Y-%m-%d %H:%M"); + if device.trim().is_empty() { + format!("{APP_NAME} ({stamp})") + } else { + format!("{APP_NAME} ({}, {stamp})", device.trim()) + } +} + +pub fn authorize_url(endpoint: &Url, state: &str, name: &str) -> Url { let mut url = endpoint.clone(); url.query_pairs_mut() - .append_pair("app_name", APP_NAME) + .append_pair("app_name", name) .append_pair("app_id", APP_ID) .append_pair( "success_url", @@ -70,7 +87,8 @@ pub fn begin( .clone() .ok_or(AppError::ApplicationPasswordsUnavailable)?; let state = random_state(); - let url = authorize_url(&endpoint, &state); + let name = password_name(&tauri_plugin_os::hostname(), chrono::Local::now()); + let url = authorize_url(&endpoint, &state, &name); *app.state::() .pending_auth .lock() @@ -80,12 +98,18 @@ pub fn begin( allow_insecure_local, }); + // Reuse an open sign-in window (closing is asynchronous, so its label + // can't be reused immediately). Its navigation guard is site-independent. if let Some(existing) = app.get_webview_window(AUTH_WINDOW) { - existing.close().ok(); + existing.set_title(&format!("Sign in to {}", discovery.name))?; + existing.navigate(url)?; + existing.unminimize().ok(); + existing.set_focus().ok(); + return Ok(()); } let handle = app.clone(); - WebviewWindowBuilder::new(app, AUTH_WINDOW, WebviewUrl::External(url)) + let window = WebviewWindowBuilder::new(app, AUTH_WINDOW, WebviewUrl::External(url)) .title(format!("Sign in to {}", discovery.name)) .inner_size(520.0, 720.0) .resizable(true) @@ -108,6 +132,12 @@ pub fn begin( || nav.scheme() == "about" }) .build()?; + // The app menu is attached to every window on Windows and Linux; the + // sign-in window shouldn't offer editor commands. + #[cfg(not(target_os = "macos"))] + window.remove_menu().ok(); + #[cfg(target_os = "macos")] + let _ = window; Ok(()) } @@ -224,9 +254,16 @@ mod tests { #[test] fn builds_authorize_url_with_state() { let ep = Url::parse("https://example.com/wp-admin/authorize-application.php").unwrap(); - let u = authorize_url(&ep, "abc"); + let u = authorize_url( + &ep, + "abc", + "WordPress Remote Editor (laptop, 2026-10-13 09:00)", + ); let pairs: std::collections::HashMap<_, _> = u.query_pairs().into_owned().collect(); - assert_eq!(pairs["app_name"], APP_NAME); + assert_eq!( + pairs["app_name"], + "WordPress Remote Editor (laptop, 2026-10-13 09:00)" + ); assert_eq!(pairs["app_id"], APP_ID); assert_eq!( pairs["success_url"], @@ -238,6 +275,23 @@ mod tests { ); } + #[test] + fn password_names_are_unique_per_device_and_time() { + use chrono::TimeZone; + let t = chrono::Local + .with_ymd_and_hms(2026, 10, 13, 9, 5, 0) + .unwrap(); + assert_eq!( + password_name("eli-mbp", t), + "WordPress Remote Editor (eli-mbp, 2026-10-13 09:05)" + ); + assert_eq!( + password_name(" ", t), + "WordPress Remote Editor (2026-10-13 09:05)" + ); + assert!(!password_name("bad\nname", t).contains('\n')); + } + #[test] fn state_is_random_and_long() { let (a, b) = (random_state(), random_state()); diff --git a/src-tauri/src/editor.rs b/src-tauri/src/editor.rs index e265d44..08c4faa 100644 --- a/src-tauri/src/editor.rs +++ b/src-tauri/src/editor.rs @@ -291,6 +291,7 @@ pub async fn open( LogicalPosition::new(b.x, b.y), LogicalSize::new(b.width, b.height), )?; + platform_layout(app, Some(b))?; *state.editor.lock().expect("poisoned") = Some(EditorSession { site_id }); Ok(()) } @@ -300,10 +301,50 @@ pub fn set_bounds(app: &AppHandle, bounds: Bounds) -> AppResult<()> { let b = bounds.sane(); w.set_position(LogicalPosition::new(b.x, b.y))?; w.set_size(LogicalSize::new(b.width, b.height))?; + platform_layout(app, Some(b))?; } Ok(()) } +/// On Linux, Tauri stacks a window's webviews in a vertical GTK box and +/// ignores explicit positions. The shell keeps the editor full-width below its +/// toolbar (focus mode, the same layout as macOS and Windows), so on Linux the +/// editor is packed at the bottom of the box with a height of "everything below +/// the toolbar", and the shell, which expands, keeps the space above it. +#[cfg(target_os = "linux")] +fn platform_layout(app: &AppHandle, editor: Option) -> AppResult<()> { + use gtk::prelude::*; + let (Some(b), Some(window), Some(webview)) = ( + editor, + app.get_window("main"), + app.get_webview(EDITOR_LABEL), + ) else { + return Ok(()); + }; + webview.with_webview(move |platform| { + let Ok(vbox) = window.default_vbox() else { + return; + }; + let widget = platform.inner(); + // Space used by non-webview children such as the menu bar. + let chrome: i32 = vbox + .children() + .iter() + .filter(|c| !c.type_().name().starts_with("WebKit")) + .map(|c| c.allocated_height()) + .sum(); + let height = (vbox.allocated_height() - chrome - b.y.round() as i32).max(1); + vbox.set_child_packing(&widget, false, false, 0, gtk::PackType::End); + widget.set_size_request(-1, height); + })?; + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +fn platform_layout(_app: &AppHandle, _editor: Option) -> AppResult<()> { + Ok(()) +} + pub fn command(app: &AppHandle, cmd: EditorCommand) -> AppResult<()> { let w = app .get_webview(EDITOR_LABEL) @@ -316,6 +357,7 @@ pub fn close(app: &AppHandle) -> AppResult<()> { if let Some(w) = app.get_webview(EDITOR_LABEL) { w.close()?; } + platform_layout(app, None)?; *app.state::().editor.lock().expect("poisoned") = None; Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e851f20..52753e1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ mod sites; mod wp; use std::sync::Mutex; -use tauri::Manager; +use tauri::{LogicalPosition, Manager}; pub struct AppState { pub store: sites::SiteStore, @@ -21,7 +21,7 @@ pub struct AppState { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { - if let Some(w) = app.get_webview_window("main") { + if let Some(w) = app.get_window("main") { w.unminimize().ok(); w.set_focus().ok(); } @@ -36,6 +36,7 @@ pub fn run() { pending_auth: Mutex::new(None), editor: Mutex::new(None), }); + create_main_window(app)?; let menu = menu::build(app.handle())?; app.set_menu(menu)?; app.on_menu_event(|app, event| menu::forward(app, event.id().as_ref())); @@ -59,3 +60,28 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running WordPress Remote Editor"); } + +/// The main window is a plain window hosting two webviews: the shell UI +/// (resized with the window) and, when open, the editor placed over the +/// shell's editor area. Building it this way keeps child-webview layout +/// identical on macOS, Windows and Linux. +fn create_main_window(app: &mut tauri::App) -> tauri::Result<()> { + let config = app + .config() + .app + .windows + .iter() + .find(|w| w.label == "main") + .cloned() + .expect("main window missing from tauri.conf.json"); + let window = tauri::window::WindowBuilder::from_config(app, &config)?.build()?; + let size = window + .inner_size()? + .to_logical::(window.scale_factor()?); + window.add_child( + tauri::webview::WebviewBuilder::from_config(&config).auto_resize(), + LogicalPosition::new(0.0, 0.0), + size, + )?; + Ok(()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e313b02..e970dcc 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -22,7 +22,8 @@ "minWidth": 900, "minHeight": 600, "center": true, - "dragDropEnabled": false + "dragDropEnabled": false, + "create": false } ], "security": { @@ -86,8 +87,6 @@ "deb": { "section": "editors", "depends": [ - "libwebkit2gtk-4.1-0", - "libgtk-3-0", "libsecret-1-0" ] } diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index c1c9868..d1f6590 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -23,7 +23,8 @@ "sidebar" ], "state": "followsWindowActiveState" - } + }, + "create": false } ] } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 319487f..34f4c09 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -12,7 +12,12 @@ "center": true, "dragDropEnabled": false, "transparent": true, - "windowEffects": { "effects": ["mica"] } + "windowEffects": { + "effects": [ + "mica" + ] + }, + "create": false } ] } diff --git a/src/App.tsx b/src/App.tsx index 1771414..be55b33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -191,7 +191,7 @@ export function App({ platform }: { platform: Platform }) { }, [leaveEditor, onSiteAdded, openEditor, startAddSite]); return ( -
+
Date: Tue, 13 Oct 2026 04:10:31 +0000 Subject: [PATCH 3/4] Document editor focus mode Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_013wWY5SvB2w46Qw92qtHG5e --- docs/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 63d1bf9..8dc6e37 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -19,7 +19,7 @@ Three ways to get Gutenberg into a desktop app were considered: | Load the site's editor scripts into a local shell page | Partial | Needs a server-side asset manifest; meta boxes and many `admin_enqueue_scripts` integrations still break. | | **Load the site's own block editor screen in an isolated, hardened webview inside a native shell** | **Complete** | The site runs its real editor, so every block, sidebar plugin and meta box works exactly as in wp-admin. | -We use the third approach. It is the only one that can honour "fully compatible with any kind of block plugin". Everything *around* the editor canvas is native: windows, menus, keyboard shortcuts, site and content browser, toolbar, dialogs, notifications, credential storage. The wp-admin chrome (admin bar, admin menu, the "W" back button) is hidden so the editor reads as part of the app, not as a website. +We use the third approach. It is the only one that can honour "fully compatible with any kind of block plugin". Everything *around* the editor canvas is native: windows, menus, keyboard shortcuts, site and content browser, toolbar, dialogs, notifications, credential storage. The wp-admin chrome (admin bar, admin menu, the "W" back button) is hidden so the editor reads as part of the app, not as a website. While a post is open the sidebar collapses (focus mode) and the editor fills the window below the app's own toolbar, which carries Save, Publish/Submit for Review, Undo/Redo, preview sizes and the settings toggle. ## Framework: Tauri 2 (Rust core + system webview) From 1b7c2df2848b75e8686023877234a660e6b445a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 04:21:59 +0000 Subject: [PATCH 4/4] Fix unused import on macOS PredefinedMenuItem is only used for the Quit item on Windows and Linux. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_013wWY5SvB2w46Qw92qtHG5e --- src-tauri/src/menu.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index b152a3d..ef49862 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -1,9 +1,7 @@ //! Native application menu. Standard Edit items matter on macOS: without them //! clipboard shortcuts don't reach the webviews. -use tauri::menu::{ - AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder, -}; +use tauri::menu::{AboutMetadata, Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder}; use tauri::{AppHandle, Emitter, Runtime}; pub fn build(app: &AppHandle) -> tauri::Result> { @@ -25,7 +23,9 @@ pub fn build(app: &AppHandle) -> tauri::Result> { .item(&item("add-site", "Add Site…", None)?) .item(&item("close-editor", "Close Editor", Some("CmdOrCtrl+W"))?); #[cfg(not(target_os = "macos"))] - let file = file.separator().item(&PredefinedMenuItem::quit(app, None)?); + let file = file + .separator() + .item(&tauri::menu::PredefinedMenuItem::quit(app, None)?); let file = file.build()?; let edit = SubmenuBuilder::new(app, "Edit")