From 923c466dea11466ed57fa8c828a0421dc345f5b0 Mon Sep 17 00:00:00 2001 From: samialabri Date: Fri, 16 Oct 2026 20:13:54 +0900 Subject: [PATCH] M3: working desktop app + browser pairing channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Builds and runs (tauri build --debug); placeholder icons; persistent VaultCore::open in the OS app-data dir; Secret Key remembered on the device; tray keeps the app serving the extension. - Auto-lock: idle (system idle on Windows/macOS, in-app activity on Linux), screen lock, sleep. - Local IPC for kestrove-nmh: 0700 dir + 0600 Unix socket with peer-uid checks; Windows named pipe (first instance, local only, user DACL, client SID check) behind cfg. - Pairing channel: X25519 → HKDF → 6-word fingerprint confirmed in both UIs, per-connection session keys, AES-256-GCM with strict counters. Test vectors shared with kestrove-browser. - Broker: query/fill/totp/save with origin re-checks; pairing and save prompts in the UI. - kestrove-nmh relays ciphertext only; native-host installers for Linux/macOS/Windows. - Tests: unit tests, tests/nmh_stdio.rs (real kestrove-nmh over stdio), examples/test-host. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 +- README.md | 48 +- assets/logo.png | Bin 0 -> 9282 bytes index.html | 1 + native-messaging/README.md | 19 +- native-messaging/app.kestrove.nmh.chrome.json | 2 +- .../app.kestrove.nmh.firefox.json | 2 +- native-messaging/install.ps1 | 61 + native-messaging/install.sh | 105 + package-lock.json | 404 +- package.json | 6 +- src-tauri/Cargo.lock | 6148 +++++++++++++++++ src-tauri/Cargo.toml | 40 +- src-tauri/examples/test-host.rs | 71 + src-tauri/icons/128x128.png | Bin 0 -> 4624 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 9243 bytes src-tauri/icons/32x32.png | Bin 0 -> 1218 bytes src-tauri/icons/Square107x107Logo.png | Bin 0 -> 3929 bytes src-tauri/icons/Square142x142Logo.png | Bin 0 -> 5200 bytes src-tauri/icons/Square150x150Logo.png | Bin 0 -> 5430 bytes src-tauri/icons/Square284x284Logo.png | Bin 0 -> 10060 bytes src-tauri/icons/Square30x30Logo.png | Bin 0 -> 1128 bytes src-tauri/icons/Square310x310Logo.png | Bin 0 -> 11344 bytes src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1658 bytes src-tauri/icons/Square71x71Logo.png | Bin 0 -> 2652 bytes src-tauri/icons/Square89x89Logo.png | Bin 0 -> 3267 bytes src-tauri/icons/StoreLogo.png | Bin 0 -> 1894 bytes src-tauri/icons/icon.icns | Bin 0 -> 74671 bytes src-tauri/icons/icon.ico | Bin 0 -> 18333 bytes src-tauri/icons/icon.png | Bin 0 -> 17177 bytes src-tauri/src/autolock.rs | 213 + src-tauri/src/bin/kestrove-nmh.rs | 87 +- src-tauri/src/broker.rs | 705 ++ src-tauri/src/channel.rs | 251 + src-tauri/src/framing.rs | 68 + src-tauri/src/ipc.rs | 450 ++ src-tauri/src/lib.rs | 9 + src-tauri/src/main.rs | 378 +- src-tauri/src/private_file.rs | 27 + src-tauri/src/words.rs | 48 + src-tauri/tauri.conf.json | 2 +- src-tauri/tests/nmh_stdio.rs | 148 + src/main.ts | 247 +- src/style.css | 13 + 44 files changed, 9289 insertions(+), 270 deletions(-) create mode 100644 assets/logo.png create mode 100644 native-messaging/install.ps1 create mode 100755 native-messaging/install.sh create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/examples/test-host.rs 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/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/src/autolock.rs create mode 100644 src-tauri/src/broker.rs create mode 100644 src-tauri/src/channel.rs create mode 100644 src-tauri/src/framing.rs create mode 100644 src-tauri/src/ipc.rs create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/private_file.rs create mode 100644 src-tauri/src/words.rs create mode 100644 src-tauri/tests/nmh_stdio.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c39788..3542eb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,13 @@ jobs: - uses: actions/checkout@v4 with: { repository: ${{ github.repository_owner }}/kestrove-core, path: kestrove-core, token: ${{ secrets.CORE_READ_TOKEN }} } - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libxdo-dev + run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev - uses: dtolnay/rust-toolchain@stable - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci && npm run build working-directory: kestrove-desktop - - run: cargo check --all-targets + - run: cargo clippy --all-targets -- -D warnings + working-directory: kestrove-desktop/src-tauri + - run: cargo test working-directory: kestrove-desktop/src-tauri diff --git a/README.md b/README.md index 2d7bb37..bb00927 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,54 @@ Also ships `kestrove-nmh`, the **native-messaging host** that bridges `kestrove- ```bash # prerequisites: rustup, Node 20+, and Tauri's OS deps (https://tauri.app/start/prerequisites/) npm install -npm run tauri icon path/to/logo.png # generates src-tauri/icons (required before building) npm run tauri dev npm run tauri build # MSI/NSIS on Windows, AppImage/deb on Linux +npm run tauri build -- --debug --bundles app # macOS dev build (Kestrove.app) +npm run tauri icon assets/logo.png # regenerate src-tauri/icons (placeholder logo) +(cd src-tauri && cargo clippy --all-targets -- -D warnings && cargo test) ``` +The vault lives in the OS app-data dir (`vault.sqlite`, plus `pairings.json`, `settings.json` and +`secret-key`, all owner-only). Closing the window keeps Kestrove in the tray so the browser extension +keeps working; Quit is in the tray menu. + +## Browser integration (docs/05) + +``` +extension ─ native messaging (stdio) ─▶ kestrove-nmh ─ local IPC ─▶ app (src-tauri/src/broker.rs) + └──────────── pairing channel: X25519 → 6 words → AES-256-GCM, end to end ───────────┘ +``` + +* **IPC** (`ipc.rs`): Unix socket `$XDG_RUNTIME_DIR/kestrove/nmh.sock` (macOS: the per-user temp + dir), dir 0700, socket 0600, peer uid checked on both ends. Windows: `\\.\pipe\kestrove-nmh-`, + first instance only, local clients only, DACL for the current user, client token SID checked + (compile-checked only). `KESTROVE_IPC_DIR` isolates an instance for tests. +* **Pairing** (`channel.rs`, mirrored by kestrove-browser `src/channel.ts`, shared test vectors): + both UIs show 6 words; the user confirms in the app (needs an unlocked vault) and in the extension. + Each connection derives a fresh session key from both sides' nonces; messages carry strict + counters, so replay and reordering fail. `kestrove-nmh` only relays ciphertext. +* **Protocol** (`broker.rs`): `query` → identities for the origin (`items_for_url`); `fill`/`totp` + re-check that the item matches the origin and need an unlocked vault; `save` shows a prompt in + the app (new login or password update). +* **Native host manifests**: `native-messaging/install.sh` (Linux, macOS) and `install.ps1` + (Windows, HKCU registry) fill in the host path and extension ids. +* **Tests**: `cargo test` (unit tests + `tests/nmh_stdio.rs`, which drives the real `kestrove-nmh` + over stdio through the paired channel). `examples/test-host.rs` is a headless, auto-approving + stand-in for the app used by kestrove-browser's interop and Chrome end-to-end tests. + +## Auto-lock + +10 minutes idle by default (Settings), plus screen lock and sleep (`autolock.rs`). Idle time is the +system's on Windows (`GetLastInputInfo`) and macOS; on Linux it is activity inside Kestrove (UI input +and fills). Screen lock: Windows input-desktop check, Linux logind `LockedHint`, macOS session dictionary. +Sleep: a jump in the wall clock between polls. + ## Roadmap (M3, kestrove-spec/docs/07) -- [ ] Persistent vault + sync (kv-store / kv-sync) - [ ] Unlock with Windows Hello (KeyCredentialManager, TPM) and libsecret on Linux -- [ ] Auto-lock on idle, screen lock and suspend -- [ ] Local IPC between `kestrove-nmh` and the app, plus the pairing AEAD channel +- [x] Persistent vault (`VaultCore::open` in app data); sync UI still to do +- [x] Auto-lock on idle, screen lock and suspend +- [x] Local IPC between `kestrove-nmh` and the app, plus the pairing AEAD channel +- [x] Native-messaging manifest installers (`native-messaging/install.sh`, `install.ps1`) - [ ] **Quick Access** global hotkey (Ctrl+Shift+Space): search, then copy or auto-type (Windows `SendInput`, X11 XTest; Wayland gets copy only) -- [ ] Installer registers native-messaging manifests (`native-messaging/`) +- [ ] MSI/deb installers run the manifest installers - [ ] Windows 11 passkey plugin provider diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..165e53468f5bd7c3be65a50ef04b5c0d3e7bbe1f GIT binary patch literal 9282 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index f2d20de..54ba9c1 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@
+ diff --git a/native-messaging/README.md b/native-messaging/README.md index f30f253..f5262e0 100644 --- a/native-messaging/README.md +++ b/native-messaging/README.md @@ -1,6 +1,23 @@ # Native messaging manifests -The installer fills in the placeholders and registers the manifests: +`install.sh` (Linux, macOS) and `install.ps1` (Windows) fill in the placeholders and register the +manifests for the current user. Default ids are the development ones: the unpacked Chrome build is pinned +to `kogapfibnnpeapfcjkkpgjddbhnjfein` by the manifest key in kestrove-browser, Firefox uses +`browser@kestrove.app`. Pass `--chrome-id` / `-ChromeId` for a Web Store build. + +```bash +native-messaging/install.sh # every browser with a profile dir, host from target/ +native-messaging/install.sh --browsers chrome --nmh /opt/kestrove/kestrove-nmh +native-messaging/install.sh --user-data-dir /tmp/profile # a Chromium started with --user-data-dir (tests) +native-messaging/install.sh --uninstall +``` + +| Browser | macOS (dev only; the Apple app ships its own host) | +|---|---| +| Chrome | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/` | +| Edge / Brave / Chromium | `~/Library/Application Support/{Microsoft Edge,BraveSoftware/Brave-Browser,Chromium}/NativeMessagingHosts/` | +| Firefox | `~/Library/Application Support/Mozilla/NativeMessagingHosts/` | + | Browser | Linux | Windows | |---|---|---| diff --git a/native-messaging/app.kestrove.nmh.chrome.json b/native-messaging/app.kestrove.nmh.chrome.json index 26533f5..6663cb2 100644 --- a/native-messaging/app.kestrove.nmh.chrome.json +++ b/native-messaging/app.kestrove.nmh.chrome.json @@ -1,7 +1,7 @@ { "name": "app.kestrove.nmh", "description": "Kestrove native messaging host", - "path": "__INSTALL_DIR__/kestrove-nmh", + "path": "__NMH_PATH__", "type": "stdio", "allowed_origins": ["chrome-extension://__CHROME_EXTENSION_ID__/"] } diff --git a/native-messaging/app.kestrove.nmh.firefox.json b/native-messaging/app.kestrove.nmh.firefox.json index f69821c..1354b22 100644 --- a/native-messaging/app.kestrove.nmh.firefox.json +++ b/native-messaging/app.kestrove.nmh.firefox.json @@ -1,7 +1,7 @@ { "name": "app.kestrove.nmh", "description": "Kestrove native messaging host", - "path": "__INSTALL_DIR__/kestrove-nmh", + "path": "__NMH_PATH__", "type": "stdio", "allowed_extensions": ["__FIREFOX_EXTENSION_ID__"] } diff --git a/native-messaging/install.ps1 b/native-messaging/install.ps1 new file mode 100644 index 0000000..7400edc --- /dev/null +++ b/native-messaging/install.ps1 @@ -0,0 +1,61 @@ +# Registers the Kestrove native-messaging host for the current user on Windows (HKCU, no admin). +# Linux/macOS: install.sh. +# +# powershell -ExecutionPolicy Bypass -File install.ps1 [-Nmh C:\path\kestrove-nmh.exe] +# [-ChromeId ID] [-FirefoxId ID] [-Browsers chrome,chromium,edge,firefox] [-Uninstall] +# +# Chrome and Brave share the Google\Chrome key. The manifests go to %LOCALAPPDATA%\Kestrove\NativeMessagingHosts. +# allowed_origins / allowed_extensions pin the host to our extension ids (docs/04 A8). +param( + [string]$Nmh = "", + [string]$ChromeId = "kogapfibnnpeapfcjkkpgjddbhnjfein", + [string]$FirefoxId = "browser@kestrove.app", + [string[]]$Browsers = @("chrome", "chromium", "edge", "firefox"), + [switch]$Uninstall +) +$ErrorActionPreference = "Stop" +$HostName = "app.kestrove.nmh" +$Here = Split-Path -Parent $MyInvocation.MyCommand.Path + +if ($ChromeId -notmatch '^[a-p]{32}$') { throw "-ChromeId must be 32 letters a-p" } +if ($FirefoxId -notmatch '^[A-Za-z0-9._@{}-]+$') { throw "bad -FirefoxId" } + +$Keys = @{ + chrome = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$HostName" + chromium = "HKCU:\Software\Chromium\NativeMessagingHosts\$HostName" + edge = "HKCU:\Software\Microsoft\Edge\NativeMessagingHosts\$HostName" + firefox = "HKCU:\Software\Mozilla\NativeMessagingHosts\$HostName" +} +$OutDir = Join-Path $env:LOCALAPPDATA "Kestrove\NativeMessagingHosts" + +if ($Uninstall) { + foreach ($b in $Browsers) { + if (Test-Path $Keys[$b]) { Remove-Item $Keys[$b] -Force; Write-Host "removed $($Keys[$b])" } + } + Remove-Item (Join-Path $OutDir "$HostName.*.json") -ErrorAction SilentlyContinue + return +} + +if (-not $Nmh) { + foreach ($c in @((Join-Path $Here "kestrove-nmh.exe"), (Join-Path $Here "..\src-tauri\target\release\kestrove-nmh.exe"), (Join-Path $Here "..\src-tauri\target\debug\kestrove-nmh.exe"))) { + if (Test-Path $c) { $Nmh = $c; break } + } +} +if (-not $Nmh -or -not (Test-Path $Nmh)) { throw "kestrove-nmh.exe not found; pass -Nmh" } +$Nmh = (Resolve-Path $Nmh).Path + +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null +foreach ($flavor in @("chrome", "firefox")) { + $m = Get-Content -Raw (Join-Path $Here "$HostName.$flavor.json") | ConvertFrom-Json + $m.path = $Nmh + if ($flavor -eq "chrome") { $m.allowed_origins = @("chrome-extension://$ChromeId/") } else { $m.allowed_extensions = @($FirefoxId) } + $m | ConvertTo-Json -Depth 4 | Set-Content -Encoding UTF8 (Join-Path $OutDir "$HostName.$flavor.json") +} + +foreach ($b in $Browsers) { + if (-not $Keys.ContainsKey($b)) { throw "unknown browser $b" } + $flavor = if ($b -eq "firefox") { "firefox" } else { "chrome" } + New-Item -Force -Path $Keys[$b] | Out-Null + Set-Item -Path $Keys[$b] -Value (Join-Path $OutDir "$HostName.$flavor.json") + Write-Host "registered $($Keys[$b])" +} diff --git a/native-messaging/install.sh b/native-messaging/install.sh new file mode 100755 index 0000000..7208cd3 --- /dev/null +++ b/native-messaging/install.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# Registers the Kestrove native-messaging host with the browsers of the current user (Linux, macOS). +# Windows: install.ps1. +# +# install.sh [--nmh /path/to/kestrove-nmh] [--chrome-id ID] [--firefox-id ID] +# [--browsers chrome,chromium,edge,brave,firefox] [--user-data-dir DIR] [--uninstall] +# +# With neither --browsers nor --user-data-dir it installs for every browser whose profile directory exists. +# --user-data-dir adds a Chromium-style profile dir started with --user-data-dir (Chromium then +# looks for NativeMessagingHosts/ inside it), e.g. for tests. +# allowed_origins / allowed_extensions pin the host to our extension ids (docs/04 A8). +set -eu + +HOST=app.kestrove.nmh +HERE=$(cd "$(dirname "$0")" && pwd) +NMH="" +# Development ids: the unpacked Chrome build is pinned by the manifest key in kestrove-browser/scripts/build.mjs. +CHROME_ID=kogapfibnnpeapfcjkkpgjddbhnjfein +FIREFOX_ID=browser@kestrove.app +BROWSERS="" +USER_DATA_DIR="" +UNINSTALL=0 + +die() { echo "install.sh: $*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --nmh) NMH=$2; shift 2 ;; + --chrome-id) CHROME_ID=$2; shift 2 ;; + --firefox-id) FIREFOX_ID=$2; shift 2 ;; + --browsers) BROWSERS=$(echo "$2" | tr ',' ' '); shift 2 ;; + --user-data-dir) USER_DATA_DIR=$2; shift 2 ;; + --uninstall) UNINSTALL=1; shift ;; + -h|--help) sed -n '2,13p' "$0"; exit 0 ;; + *) die "unknown option $1" ;; + esac +done + +echo "$CHROME_ID" | grep -Eq '^[a-p]{32}$' || die "--chrome-id must be 32 letters a-p" +echo "$FIREFOX_ID" | grep -Eq '^[A-Za-z0-9._@{}-]+$' || die "bad --firefox-id" + +if [ "$UNINSTALL" = 0 ]; then + if [ -z "$NMH" ]; then + # Next to this script (installed layout), else the cargo build output (development). + for c in "$HERE/kestrove-nmh" "$HERE/../src-tauri/target/release/kestrove-nmh" "$HERE/../src-tauri/target/debug/kestrove-nmh"; do + [ -x "$c" ] && NMH=$c && break + done + fi + [ -n "$NMH" ] || die "kestrove-nmh not found; pass --nmh" + [ -x "$NMH" ] || die "$NMH is not executable" + NMH=$(cd "$(dirname "$NMH")" && pwd)/$(basename "$NMH") + case "$NMH" in *[\"\\\|\&]*) die "the host path may not contain quotes, backslashes, | or &" ;; esac +fi + +case "$(uname -s)" in + Darwin) + BASE="$HOME/Library/Application Support" + dir_chrome="$BASE/Google/Chrome" + dir_chromium="$BASE/Chromium" + dir_edge="$BASE/Microsoft Edge" + dir_brave="$BASE/BraveSoftware/Brave-Browser" + dir_firefox="$BASE/Mozilla" + ff_sub=NativeMessagingHosts + ;; + Linux) + CFG=${XDG_CONFIG_HOME:-$HOME/.config} + dir_chrome="$CFG/google-chrome" + dir_chromium="$CFG/chromium" + dir_edge="$CFG/microsoft-edge" + dir_brave="$CFG/BraveSoftware/Brave-Browser" + dir_firefox="$HOME/.mozilla" + ff_sub=native-messaging-hosts + ;; + *) die "unsupported OS $(uname -s); on Windows use install.ps1" ;; +esac + +# Auto-detect only when nothing was named: a test run with --user-data-dir must not touch real profiles. +[ -n "$BROWSERS$USER_DATA_DIR" ] || for b in chrome chromium edge brave firefox; do + eval "d=\$dir_$b" + [ -d "$d" ] && BROWSERS="$BROWSERS $b" +done + +render() { # template → stdout + sed -e "s|__NMH_PATH__|$NMH|" -e "s|__CHROME_EXTENSION_ID__|$CHROME_ID|" -e "s|__FIREFOX_EXTENSION_ID__|$FIREFOX_ID|" "$HERE/$HOST.$1.json" +} + +place() { # flavor dir + if [ "$UNINSTALL" = 1 ]; then + rm -f "$2/$HOST.json" && echo "removed $2/$HOST.json" + else + mkdir -p "$2" + render "$1" > "$2/$HOST.json.tmp" && chmod 644 "$2/$HOST.json.tmp" && mv "$2/$HOST.json.tmp" "$2/$HOST.json" + echo "installed $2/$HOST.json" + fi +} + +for b in $BROWSERS; do + case "$b" in + chrome|chromium|edge|brave) eval "d=\$dir_$b"; place chrome "$d/NativeMessagingHosts" ;; + firefox) place firefox "$dir_firefox/$ff_sub" ;; + *) die "unknown browser $b" ;; + esac +done +[ -z "$USER_DATA_DIR" ] || place chrome "$USER_DATA_DIR/NativeMessagingHosts" +[ -n "$BROWSERS$USER_DATA_DIR" ] || echo "no supported browser profile found (use --browsers)" diff --git a/package-lock.json b/package-lock.json index 693cad6..d0adef7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@tauri-apps/api": "2.1.1" + "@tauri-apps/api": "2.12.3" }, "devDependencies": { - "@tauri-apps/cli": "2.1.0", + "@tauri-apps/cli": "2.12.2", "typescript": "5.7.2", - "vite": "6.0.3" + "vite": "6.4.4" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -35,9 +35,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -52,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -69,9 +69,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -86,9 +86,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -103,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -137,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -154,9 +154,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -171,9 +171,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -188,9 +188,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -205,9 +205,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -222,9 +222,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -239,9 +239,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -256,9 +256,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -273,9 +273,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -290,9 +290,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -324,9 +324,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -341,9 +341,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -358,9 +358,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -374,10 +374,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -392,9 +409,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -409,9 +426,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -426,9 +443,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -810,9 +827,9 @@ ] }, "node_modules/@tauri-apps/api": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz", - "integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==", + "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", @@ -820,9 +837,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.1.0.tgz", - "integrity": "sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==", + "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": { @@ -836,22 +853,23 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.1.0", - "@tauri-apps/cli-darwin-x64": "2.1.0", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.1.0", - "@tauri-apps/cli-linux-arm64-gnu": "2.1.0", - "@tauri-apps/cli-linux-arm64-musl": "2.1.0", - "@tauri-apps/cli-linux-x64-gnu": "2.1.0", - "@tauri-apps/cli-linux-x64-musl": "2.1.0", - "@tauri-apps/cli-win32-arm64-msvc": "2.1.0", - "@tauri-apps/cli-win32-ia32-msvc": "2.1.0", - "@tauri-apps/cli-win32-x64-msvc": "2.1.0" + "@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.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.1.0.tgz", - "integrity": "sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==", + "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" ], @@ -866,9 +884,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.1.0.tgz", - "integrity": "sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==", + "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" ], @@ -883,9 +901,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.1.0.tgz", - "integrity": "sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==", + "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" ], @@ -900,9 +918,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.1.0.tgz", - "integrity": "sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==", + "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" ], @@ -917,9 +935,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.0.tgz", - "integrity": "sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==", + "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" ], @@ -933,10 +951,27 @@ "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.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.1.0.tgz", - "integrity": "sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==", + "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" ], @@ -951,9 +986,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.0.tgz", - "integrity": "sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==", + "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" ], @@ -968,9 +1003,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.1.0.tgz", - "integrity": "sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==", + "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" ], @@ -985,9 +1020,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.1.0.tgz", - "integrity": "sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==", + "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" ], @@ -1002,9 +1037,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.1.0.tgz", - "integrity": "sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==", + "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" ], @@ -1026,9 +1061,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1039,31 +1074,50 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/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": { @@ -1107,6 +1161,20 @@ "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", + "peer": true, + "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", @@ -1192,6 +1260,23 @@ "node": ">=0.10.0" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/typescript": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", @@ -1207,15 +1292,18 @@ } }, "node_modules/vite": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", - "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz", + "integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.0", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index 233d4c0..7ff3ce8 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,11 @@ "tauri": "tauri" }, "dependencies": { - "@tauri-apps/api": "2.1.1" + "@tauri-apps/api": "2.12.3" }, "devDependencies": { - "@tauri-apps/cli": "2.1.0", + "@tauri-apps/cli": "2.12.2", "typescript": "5.7.2", - "vite": "6.0.3" + "vite": "6.4.4" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..5bd292b --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6148 @@ +# 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 = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[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 = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[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 = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arboard" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "async-compat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c97d7ff3c25d6c10d64170c12acaf5d4245e76dece3779c1d92b153a64f11df" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[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 = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[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 = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[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 = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "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 = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[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.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[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 = "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 = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[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 = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[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.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", + "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", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.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 = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "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 = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[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 = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive-where" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2b94854e8576378ccda7c8de8a66ed8b4e8acbd2c50ec3418ea6c8aaf4b567" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[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", + "const-oid", + "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 = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[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 = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[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 = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[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 = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[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-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 = [ + "serde", + "typenum", + "version_check", + "zeroize", +] + +[[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", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[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 = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[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 = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "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 = "hpke" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4917627a14198c3603282c5158b815ad5534795451d3c074b53cf3cee0960b11" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "digest", + "generic-array", + "hkdf", + "hmac", + "p256", + "rand_core", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[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", + "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 = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[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 = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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 = "kestrove-desktop" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "arboard", + "base64 0.22.1", + "core-foundation", + "getrandom 0.2.17", + "hkdf", + "kv-ffi", + "libc", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tempfile", + "url", + "uuid", + "windows-sys 0.61.2", + "x25519-dalek", + "zeroize", +] + +[[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 = "kv-crypto" +version = "0.1.0" +dependencies = [ + "argon2", + "chacha20poly1305", + "ed25519-dalek", + "hkdf", + "hpke", + "kv-proto", + "rand_core", + "sha2", + "thiserror 2.0.21", + "unicode-normalization", + "uuid", + "zeroize", +] + +[[package]] +name = "kv-ffi" +version = "0.1.0" +dependencies = [ + "hkdf", + "kv-crypto", + "kv-model", + "kv-proto", + "kv-store", + "kv-sync", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "tokio", + "uniffi", + "uuid", + "zeroize", +] + +[[package]] +name = "kv-model" +version = "0.1.0" +dependencies = [ + "data-encoding", + "hmac", + "psl", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "thiserror 2.0.21", + "url", + "uuid", + "zeroize", +] + +[[package]] +name = "kv-proto" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "opaque-ke", + "serde", + "sha2", + "uuid", +] + +[[package]] +name = "kv-store" +version = "0.1.0" +dependencies = [ + "kv-proto", + "rusqlite", + "serde_json", + "thiserror 2.0.21", + "uuid", +] + +[[package]] +name = "kv-sync" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "blake3", + "ciborium", + "kv-crypto", + "kv-model", + "kv-proto", + "kv-store", + "opaque-ke", + "p256", + "rand_core", + "reqwest 0.12.28", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.21", + "time", + "tokio", + "url", + "uuid", + "webpki-roots 0.26.11", + "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 = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[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 = "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 = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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 = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[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 = "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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.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-core-graphics", + "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", + "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 = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "opaque-ke" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb31f7b2c5760d8ffbc39652043f5cff14029b6249a2280b28ae6f0cd61f5098" +dependencies = [ + "curve25519-dalek", + "derive-where", + "digest", + "displaydoc", + "elliptic-curve", + "generic-array", + "getrandom 0.2.17", + "hkdf", + "hmac", + "rand", + "serde", + "subtle", + "voprf", + "zeroize", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[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 1.0.4", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[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 = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[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 = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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 = "psl" +version = "2.1.241" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7319b480e204d67e759cad7886178335d26324c7c8573c126cc9dfa7350d064" +dependencies = [ + "psl-types", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[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", +] + +[[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", +] + +[[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 = "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", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[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 = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[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.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "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 = "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 = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[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 = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[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 = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[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 = "static_vcruntime" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" + +[[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 = "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", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "static_vcruntime", + "strsim", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata 0.19.2", + "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 = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "smawk", + "unicode-width", +] + +[[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 = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "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.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[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-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.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 = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata 0.15.4", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata 0.15.4", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "once_cell", + "paste", + "serde", + "textwrap", + "toml 0.5.11", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher 0.3.11", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata 0.15.4", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "voprf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f59c30c76e2fea54cdece6a054e2662feffa7ab19658a7887524265ee39470" +dependencies = [ + "curve25519-dalek", + "derive-where", + "digest", + "displaydoc", + "elliptic-curve", + "generic-array", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[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 = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[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 = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[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.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", + "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-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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "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 = "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 = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[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 = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +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 = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2114229..52fa1d2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,6 +6,11 @@ rust-version = "1.82" license = "Apache-2.0" publish = false +# Shared by the app, the native-messaging host and the integration tests. +[lib] +name = "kestrove_desktop" +path = "src/lib.rs" + [[bin]] name = "kestrove-desktop" path = "src/main.rs" @@ -21,10 +26,43 @@ tauri-build = { version = "2", features = [] } [dependencies] # Repos are checked out side by side (see ../../README.md). kv-ffi = { path = "../../kestrove-core/crates/kv-ffi" } -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon"] } serde = { version = "1", features = ["derive"] } serde_json = "1" arboard = "3" +# Browser pairing channel (docs/05): X25519 + HKDF-SHA256 + AES-256-GCM (what WebCrypto offers on the extension side). +x25519-dalek = { version = "2", features = ["static_secrets", "zeroize"] } +hkdf = "0.12" +sha2 = "0.10" +aes-gcm = "0.10" +getrandom = "0.2" +base64 = "0.22" +uuid = { version = "1", features = ["v4"] } +url = "2" +zeroize = "1" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Pipes", + "Win32_System_Threading", + "Win32_System_StationsAndDesktops", + "Win32_System_SystemInformation", + "Win32_UI_Input_KeyboardAndMouse", +] } + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.10" + +[dev-dependencies] +tempfile = "3" [profile.release] lto = true diff --git a/src-tauri/examples/test-host.rs b/src-tauri/examples/test-host.rs new file mode 100644 index 0000000..a0576d3 --- /dev/null +++ b/src-tauri/examples/test-host.rs @@ -0,0 +1,71 @@ +//! Headless stand-in for the desktop app, for end-to-end tests of the browser extension and +//! `kestrove-nmh`. It runs the same `Broker` as the app over the same IPC endpoint, with an +//! in-memory vault holding one test login, and plays the user: it approves pairings and saves. +//! Never ship this: it auto-approves. +//! +//! KESTROVE_IPC_DIR=/tmp/kv-e2e KESTROVE_TEST_ORIGIN=http://127.0.0.1:8765 \ +//! cargo run --example test-host +//! +//! Prints `READY `, then `PAIRED ` and `SAVED ` as they happen. + +use std::io::Write; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use kestrove_desktop::broker::{Broker, PairingStore, PairingView, SaveView, Ui}; +use kestrove_desktop::ipc; +use kv_ffi::{ItemDto, ItemKindDto, VaultCore}; + +struct Quiet; +impl Ui for Quiet { + fn pairing_requested(&self, _: &PairingView) {} + fn save_requested(&self, _: &SaveView) {} +} + +fn main() -> Result<(), Box> { + let origin = std::env::var("KESTROVE_TEST_ORIGIN").unwrap_or_else(|_| "http://127.0.0.1:8765".into()); + let core = VaultCore::new_in_memory(); + core.create_account("test master password".into())?; + core.save_item(ItemDto { + id: String::new(), + kind: ItemKindDto::Login, + title: "Test login".into(), + username: Some("alice@example.com".into()), + password: Some("correct-horse-42".into()), + urls: vec![origin], + totp: Some("JBSWY3DPEHPK3PXP".into()), + notes: None, + favorite: false, + tags: vec![], + modified_at: 0, + })?; + let broker = Broker::new(core, Arc::new(Quiet), PairingStore::in_memory()); + let listener = ipc::Listener::bind()?; + println!("READY {}", ipc::endpoint()?.display()); + std::io::stdout().flush()?; + + let b = broker.clone(); + thread::spawn(move || loop { + thread::sleep(Duration::from_millis(200)); + if let Some(p) = b.pending_pairing() { + if b.resolve_pairing(&p.id, true).is_ok() { + println!("PAIRED {}", p.words.join(" ")); + } + } + for s in b.pending_saves() { + if b.resolve_save(&s.id, true).is_ok() { + println!("SAVED {} {}", s.origin, s.username); + } + } + let _ = std::io::stdout().flush(); + }); + + loop { + let conn = listener.accept()?; + let b = broker.clone(); + thread::spawn(move || { + let _ = b.serve(conn); + }); + } +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..47a5ab62d745381c0cb72e917853346f9cfc2845 GIT binary patch literal 4624 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 AkpKVy 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..44fbe11110c8a62ab1b70eb44fe398ed92c20493 GIT binary patch literal 9243 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 W{q39ahd&Md0F+*-%U8-;1pf~S5vJGx literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..8657b846b54ecb7a11ee578f98c27124f40c219f GIT binary patch literal 1218 zcmV;z1U>tSP)9vPfvHxW|?Xp6t7;@@74RPwkT-`Aq5l7 zU}&vf0>#0^0UN&74$X21p`c4t5or{7-jVZFhS zY$l_8HbVs_$91QK0xQlKz=}&l9cY_oeyrc^^=a{ud3x#0X~X;dN|QGK-=<0-=Vfg2 zZtr}PqY(u>?F6|(Qc?lr+DIJ+|LE;k==^)HQzoA`{Gn~rrx$;s&%V1vB`D$Qq{?70 zMCgJtS_G4PXemi;27l|tXXxV(-Z8vhr)vmv@UR9M$R@|b_~m5CIH^Gdx4`#jUwfXV z9UAUC1_*>XcvyoBO$*3SjD;(qF^`GWNDP<_?At;vOZ8)A#&S9GLuSOBd02xCY;6LX z#$6$_he2^J5F-~zvpG!AT5-DBgMGTO*Wxr#4hk0KrY+(p>mN* z`7DELLzshywdsvXjt7mI>D^B*P|)}w-t77W>i2?d;i-SVpBxQXJd(Bo>?v4;n~)a2I$NtD`+g;5q*2MYM_^!Cl>241gHDn(NU z8W5>C3#Gr_@iqkpu>1|K#9s^FT6K4CHunDgb3Iq8aIQJFjH%_Lnx60_$s51(nK&C5 zzyhWYy^5i?zSG?NvHsMvXN$FyHFtjyJj-2E=#NBX2;r40&t*Ed|GVD!dTqtsA9e#w z5eh=q$aOM>a((g5_t)kZpIh}GsdVHQvi2G>`<%8n|5)Gr{>yX2UVA6N3c4^wmB74I zUsyV}@c1(StW>8w^wOLz_$Q=wck9|lXJ@Ms#h6fFl#h5j%=qWj_&nz4RHST>6Ls@w gelXFl6MoO{16RNO;q~ez(f|Me07*qoM6N<$g8s%*{{R30 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..007cf88ef9178fe9763ce344f565314cecdedc1c GIT binary patch literal 3929 zcmV-f52o;mP)uCPv#5L%>22C>BarXbA)fS&1ZMC1fR%kd??nBTJAFg1-cLe=Rm~roSAA z|2SuV)e11fR|$et@8T``tcmP9V<7wPgi4$U@ggCrW83ERuQWZc6^g61Li=hpo9|Ur zE$g&!g{Yzo#=&Qn%oaL8(40)7L8Gz522j;g`)CoqRD@o-2ovGK;AYx%ZwCX^_dfpawaQZl zMvM>%Nf)5ZfhDP#oNkN`_xJz(xh?#Cq#42jfy+x2dp9moE4F_74JDP$*hYn|NzoSx zwyh)0whcv3>vpwNKDp=Vjgl}APro{pAWT6qroP_W?%v1W2~j}?I^zVqfSZS3mm~^K zTIggst}D0v=54!D#ihMY)i4xA)m;}S`SJ4uynq|<1CI5$`VrOVsLRj#U!-s8x4tf1f>@0^A&0oLk)a z5{G9$!5;L)hkx9;M()EroM)@la2gac)od5x^060@r$+ z=Rg)K#eRxINbT!3Z07|MN|Uro2m%~|CvXM6t{Xr5)<)o2x13 z*F-{Li>=E9uE2Nv_~EB(cvCZ$<#-|f!o}s zBa@F>Cp*#_oGwv%8?J~U;IJ&BA17?SzJFB z^M$BCv^-a`;x@Y2I$$w6Tg&#ZY^HDBavfdDB#{hSa{$1y-CYKE{_;zD>Bei;(|upP zk^al1J@ev0`ooK_Q<=-;nb3u1WxmOZ!zct%XV^NfT-8IrymuRIxa<-p$Dm57;w4;# z9H2G-!rd7!-|eP%J{+Kqd{$1Qks4NjvW@du^XRvft zWunS-^XD$5KRtXe6Sa~`lao}b8th;QxqhjN>re>Ku~5C==76)WkbQLMhu69K_0pYigHlQEvy!Y{G z*{(C61{NSs7~=ylh~L??q>ux1VcnMdX{?}^^`jk2vJ~G8gAdE{>sYpeE-`P131m1i zf?l4*HmpFd7ZslGs5|yUgX5DtS6MA>??v1)dwR#o6%|atB0vPH5KYK zRhwp^7y8kT<2Vr?_?#-EA3d>~hDXLJ%itPK5{7!H4?WOVlDRQ=dYyLJXS+6

H%p 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 baL4kB?pKy^Mk@d9hduzQXe(DMT8I4)8pMR& literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..977ef457bb6aab3536aed75570c287f9ca85cc48 GIT binary patch literal 10060 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�PK`{Kok<0kY@0!t@1q`c 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..68d2a01e1aa9a0aaf5ef37bf60b6186db362f354 GIT binary patch literal 74671 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 RUTE3-{$Vtz`=3vP{uk59A;SOw literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..32227f59fcef2530b4fd6c3e461c67c24f57139b GIT binary patch literal 17177 zcmdVCWmr_*_cwlK7`hac5b2>)Q0Y)qIz&RcLApC;&|8u2l9rM#0cjAB?iLB9TN(yt z=6|@qzwdQDZ=UD+zkHrI!(q-oYt~+C?ay9&?aw};8fx;SME8gQ03cOVc%}&eQ1B88 z5a5A-PJAX&0Pu8K@!1nCpPzdRgb7AdzROpE!}Rw2gmCyjq~?MYLe_Y!1g|XE2ZBZE z9c;w<{4-3`;EucJ(*_4+Giu_-ecQ$@jsA1{B=A|Oe?*DrLn$o;P4VZELrioQg75yJ z`6mTV9HXVFy|6{n(iS*e#I5=!d%wjd3yl?fuDuYKAnu>#-|9SKg$n@buL=kNz=fO; zRE>-U0s!q#;NYd`M;Q2t9}9jgGf{w__n(1%K)zD|FI{f^U*7oJKrR80XCBrr1qZs< ztg;x7K$cmETslC$q0))Nr|V4x$`;-);=vh(P4l!;fnfxCXJ1O`CR7aYX+|7y>kA0 zV&j{;Q}3`l)U2asjI#{4g>{*Y{yO~?ZxYp6$tB>Ed9-HltGYE(CHe3yo@rBFLtEzQ z(??hLnYk*9FkDdoZKZ0Lh3bRdOPn@Mx>t>-GH&Bn(nSGnEY3>w%QDOzCUMB|(T`ux z;_C|n#P zGgSS_F~?ru>Xy0O!YkNidDg?tgR>9gd>8uYFWyG_Qf_^|wXDgRx+X{a1D;e%oaE6_ zN)@fmz8iQx=$e+rm_8TC6giVx)2a8$7Q=V9qvuIXJ_R|9D*L~R5RXTH9UZlliPV=m zwl(-V9w%2^YnUDK><>SR{@(~a#jSp;bRl<=|4v7jJImH=>GI?dty!&*kRe+SF z_{&5R;9TtHl)c2-{%Qmu)1Lk(C5FJrEUp{-?5`~W) zFJ8>1{FK~SVy5log33ABeHY-NRi2mFL|B^69oc*EV1?SGHO7rxxQWCI7)=^iEg6uX zG6v-Xp__H9v~2PCr*aA(?&qhq3+SHs<}XHG7%9O$-|3xh&&mY1U_vw zcCt_T@8M9X7qVN9t_M^XXgkLMkM7hg`k)b8ktF;O>b?~HJ6{P=K$v;7WwzZqFYZLV zvx<|?pY@C##;d23PJ1yj3M`}ku;j%%$E{GTDl5gZdz@1nU zZ@(Z#%t(orcBOq!l+g|X^E`tfAoN-GxdPB=(Cm*JM1SHu-mwG=3hYcvy~-TK5hOl^ zv_q!3zVOZW<(kMUYCm{802|78bDRB8=kR0By&+tS#ET~ z!`NK*P*jr1;yRY>dTU)WK@8Y^<@PYvnIgEDsE~us`Fc}=F)e8A{rp;?dsdat2EmJC z&9L#$(QC31-&%d<-mesz{IvlCvPo30Uw#=MDJPsp&`a_ChrPf;+sYxn%*-#;&*=C= zoamu{)}Ms3_}F;7(4P?boVqF!^z#zowu-BZcJ%!HXXTHVG6i%FTJ!8LD8np@EkSkE z@(oilzogye=+Ry5i?iDfPQGi>dTKF}{z=&C@#C7yRPuBJ# zQrXkF{cfX(Y0Ez8Y(i=K=ZVvp{&*JAO$ldfzU}1)Jr!cxM<#7@WVJ*Fkx8`SP~aqj z!QL^^4(HLF>}m3O(6QV^MXOKYCFBb4{zD4nJ8m|2t%|K9lYlMHB?jZxKXS2its+E{ zS!Mo^Y{qJvN|U1EzQ~<}SyZPpy7;mq-RALGG&2%M(HXwAD6>7jvri*-*uog0KM_3m z;xvyK07j|%0(j=ML1(RRgSvbt(lW2`YlOo4g7Nuf!j>J0I2Jt2mp50+^#itQ z>#q{^ACdip;GY4q!wPa`hJ5lFI{rK4&TVyvDCP4?*U|Z0+$LwgJ4IT_57_CiD-7Mb zoqHp-!aB)17=a@NKb+4@e+Q=e|Av-GFaAWKSTi!_MS5$}#UNKN$lHzC%#;518Dpxx zUMCy7q8I$q6^j(2woesO0HzW6?3LSUWyNE}0iCY0x@;pyc#mes!tK#@CF|%HaWGz= zrEMAMBQ)c;?(}K-zxaTJ0;^ioz3HrwXoJHvA)}Ip(<%IM8-KUgba9_Q6IeTaO3i0g z#!OW#Nx6*|>EgyqSh&|zR*?gPD|(q6I8&^@uEiCQzF#Qrr)EhfsFf$nwYgB*XG| z*h8KQl6&!v6%LQjb3$OiZoXWLNz~7>IyIKSQS+8>c?X z{8xXyKd6uLVQuNJ=faJQcfq0Web`7Cy3 zPKe;bwRO6@?nV7zhqSdb;;jrIDlc=zhNr+AlVWNt{NTI0HA9L-$H@H!x*mBWuZ8-d z*w#Hf{3`%CFpl%jc6hW{gHYYy(xS*6=K=Hg#K{NGfRli>&pjxKcRlHq)|QuCj6`&= zg4Hkt7+_hPo*2%q;4tes7;j%-68L+V*ZK}-oEQ<@n8>(Om4W)4t1Dn|JWu{3hxamJ zcR0g|0cf9BG=KY**re%rn)s7*Ge(0-A}nC)eHPBG63btR-+I(oo2|JqJtJ8#CI^os zLrE!E;X1uMdb^y}Pe&l5iF)y=8N_M^=z|`0se}6OKP%0?q3>bEpRkTjT(;CFNU=MR$9gx5@Zy^VXuNn|ZG8 zG8#f80TY}!MXW!^_Ek)$Zw^C%W?>2*xV6G=Z>zv|Embu>)50Y4q!AWj-iRsOLg54V zQ)r1ccOTu+=skvpB?E9_G}OqKdb=FkmcPRYxNx4)5M8vA;NEdXAfb3a^4?Nqy8h0( ztZgJpo5&g7?6;mbS;yc2i)#0H%oqL$OyTuYC>vwL~2S(6CfJGY{)Z`s_$=k_}^;+gYlZ{y}B(b^WmwnZMg^ zB`eS#aKx6Rpb|T)`Lza0M9CzzB+Q4ddNqu4uyyfaB6=Goc6J*;cIH@o?4k(eqA(a# zwk_p)`xXvZQx`@JkN)V<$@F~n%a(^70|6GfLfI%@!Rr(v6o&~oU#wK$dZH;*EgB}# z_*VKAy`CV$!vb-)6uH~wq`;`I@%?}3L-`u1mkaiyWlk=6*Jp#wa5o_-j_Bwtw=>n3 zh1y^E?*Xz1MNl`P)}!5#J4Mv(C>Rs*ndXDU*h5$0vk@~H0O=b)h#N!0N~(IS1iAMq zi34!|f$^LnDMJ9VB9<+H(4=94F;GGY=wkuoV#++Riw?6NgjWrCgT?{;Z0e}9Y1|>D zsz9@cJi8Rb@!Z!_#!=8~9?@I41mg`rUn@X$K=#HFdaLIp(~hvbx-|MnZ|Rl5G*u*k zypDa%`as@1dX81LhsB_xdl=TP{P?m+K-|yM*TpiHADc2Q17u%WUWk2UJ5wN0GOl)C z5tE=qiq35!e$%!c6&g2`hWV%o15AY;gR!%$4>&KStA+69QUC>I8Gcd{figaERKQkP z)uSsY@LHkgey9}1J2(}W)kg5~G#fC-fbHzbxQ-ofH)SJ}v+pqh0IxVuih_A;fb+_w z@HG0nYl6x*8owFBnJ)@aHjh1`BOG<1KxJ&ID|@hf4^!xK%AKIIZr;apU3D;~Jq)A1 z__veU0rQ268NFngSIpol5!uBM`h+4J&i+2KXox%VWu4(WZ8<=XdZ70xk&$Z(U&;1d zCWjFiwa=u*8?90>kMr%QHUmm1kfq^Pe^XW}LMQeSX+fYbTop*-Uc(i~HIYuk~66sT5F< zf1qn`fukJVBpKI-jkrH#!5#dM-ElN&w79*1&9q`hJ0e6h6uT9Jo zMBK$4G4e(3S%6Qg4UzlF>V%9QEd!&r**I>@`4SzlOhg~=;Sfi|R@L7e_oH00#-O_{CDCXr9o;9jdq0A=|KDUdNu|m#-w#1H&M#F$hQw5RJ(&Ivj=Dkh7A$y1EC zS2y>MNyz`XmXQ6+SxfNUT_k{PuHn>~8^c-NJER5wsfO!};eeHjfE@g6Vo{P?pg5{- zZgvP1tX1$(03!iDVXgomo9S!&wK`ZT1Ofp}amjGN`i0o@|KCN7?CjCp>FnYFfM zPBWHlyfp~D`QD3jTYgQWxz@Ivyi#LWctqomsobxIGari08Y>;$fAmq|kB&dXPGn=kt zFJ6^0CYhIJ>Pb_nQ}Z%Zn=5g!r>%AQq4rd;{#$pN=XM=`vyVz}P$r>ghthEb`z3Vc z4x0#feGaNt-=$hl^TTLz=^SyhH`1*|ZBCoU|M1Owf5Ku}aXgPV>=LULsXl*H0rn`vNO|@;s&pEqA+(pb_r)s6fU0R8b_un3gB`PR(cIsfc4r-ArdcO3dYNj;Z0T zg3Di%OU{dRueAHwCc>GpA5xWN>w)wYy{Q*g6ihL%#)*<;e@>U73otZv#U%skh-g%z zc29}@u=I1%>1t%fxG7`F`pKlqU$qbT)>&W%uHYgaqjqcPl3&Nn-){YYe5%CGC%ZuU zGDmsPKC98M_S3W+k+&2o0yQ=vP+aSGVcMf5H{}^EtBp|j!_q@*Yik6B0^2WNwPLz@2pU?iBmYYv0AmX^lYbFTbCi~Q(Xf>@0 z?buYHdXBL=kWfTp@C7sq`AiD%KAg-3-b#6JSt*qY^Ft^1vzeTt&J#$Om5JVZXv>Nw zUaQ5G2B12Vi|9@?Iw z8A!fwraxYy%d4B5?puB7o82^5kA zTeF>WN;gF>nJHTBE?7;iD8BSD7e9Qj+*^^&+CS0;~Z(PvDEF4?}<-Kg6$r~ zxdsv@9go#65yxdGoX86?GVA3qg$}aWlxt=++toJi1@}yvEM8i;4-tRBA6z2T69Z0g z34u}k@u!)i7T14FGR_!IL+tsrHp=zQeoWIfN*L*11&NnCm+;!!s@7}W5t;f%c7gC& zxV!x;ez6Smlml`ubJ~1)nZmFblSm5qg~Nc&90zOcj9re_)M{e8s?v-$6^UmvYp9kZofG0pI=9c_`D6b+lW zrlTsb0a<>_Mbv{Yo;ko_9$8lE3=9O7;T{P2Mgs${p{uE5ouUdfqAeF((G@f;r4@Q! z)hX4{ovG+4q$gH6=lwM@W3T|F3S{&4AvZ7zs-y0ot!!n8l zYN1bUqzl9|WFHmWdw;Mmb4Qa(lU?(%<_k?7O>0fBwTDkW8I;Qg&D||2rGY9uez*{5 z@QP`qKS)uv{-^c)FM*&xGn4r?<>N++eY=>?sz+(bN8?s;eDuxN>E}fT7)Qmx^(2H& zd}PwSQf(V{{5TlXfrFB^F9qaSM(Qz*VV zUMQhDL5PhId6zd#`%E(NjVSYm^*gN6>0RL{`XXxyrVBXL1{itCx}j+9kdk z3?S70+R)BRY!oUYw+JsDNn4k{Q|vfto3-)It?MS^bF6jPPwl<5_WD%+qgn+gC@4&HRx$Bz)f zVw~X;*JF1VUd>5EG|x2gHL0aRDZQd31fsBNtGzm-iS6wg-Qs~$`MORmWqt`2fOea`|6Ro_JzFt`)w8@MSuMvcM)M(_Z_|6<1C%k|2J#nvnzNhv0~Gbn(4 zRLD?RP@s366moI#c?-d35+30wxVIf9>>1PGS8FFiAmq#9$?VDE$?6IB><%MYZU^o0 z?gXE)S;Jdo##2PtV!)U6-`lX|r%8SY?&l~>2 z0VXVDkTW^)SgW16u_p`FhH?YrRGPY)HaU8vfGGl$jOI*?vqH3CJBTM6gbt&t)&gdd z%jt?(i+GBJi<&4X9|fcTEI@YO@jkGx!#OQ~zOtQ5osskXEP>k{=D4p&uV|#0p}3$( zU#%)w>Ffmu+>=j_I^@ZvSCOeN3Uj4{=H_njRI7=S`_D(*Xd)x|n_t)T)(ly%WZJit z_B(wbV!f6QhN5J^nA{VZV%$!gZ4|NktKx}*oh6ona*uzdD;YthEXH!*G6SaM*1Qc< zFAMo(^!dO^Z4c>WnWpeRIw%>%wL|?kG%qYnD`9gvlxiQ!BBZe%@@rXCzoGZq_v}6MkbuB_pK_ zr3I7<9?TkaESHIVgxalQVs*W8t^9(g^pd6`uR0a@}st$z7$C?#(0Lj(zLl`H#S=ZXq3x|`sflH8#s1b)33809)S z0cw2yblA>O$kNg=&{;Tq`%>M&aHBjgE-VEH7(4+gVU-uByq=O1IDlvr4A7vs4`;b` z_x_iyujLTX#sAyA&FT%o?9`z|y{7QXw)y}ntVHLz<=jAp<7De{kv__2sr6|Wa!dq&@xvxoQ?6Qki)r3V5j4yIR zPfuKA;f@Eq3L9PXANt@Q&J29x3yj~N7Ruwu_W2Wda&qhXw%j==KBTlAjR6_)Sp+cfA|Bo7{GK7$cj0cdj=FvyF0y)M_LA>p-~=l{*L+Ki#dpvWQ#fD-xvNDnSknjghFTpSzS6AHX! z>Z$)myfqt8Fm0QI&R8eESPlAynW`|z^WPz+M6YZ#(fN*l z!BGcYmjt?r7HH(sNz_zyTx@tHRcjmGcoCt^9g!w3!T<6h?yVoOK@M7~3r=b2I?B`$ z1;SZLlM~_!T^S#^lL;`-nL$k8_T=&8^&DcB-6U&@ zN-?Kk%6{IM_N|~PlrhLF{%3Vy^8W4Yv~L7?WcmM&>HH|x58l8#7@*K6*nZac0d<2x z#`)#nIX`Rga{P-j+@1zGHuxz|py1}RFm<~wxpd7-Z(0%UqOSQ)FU=6mWX-|?-H_0( zc9KjNS*6AgJ5yn(E%ypR5)Ht;33~$^TNr$6a8KRcvJ+Sm?gSovfbb@HU}sc}#?j=v zkuV@duJNN)#zp7O<1Oc>YgdC*^@&9iMxbQtJ(&e;yVI@$;DeVdpBL@}rjo7LF1*wB z6*{dlNCKuYz_9&Vbd25yXb+RdNQ7aOaN;3Mze%w>7|H-<_b zfZV7#`>qS6-%x7&vr%>7tbJsn4+#hAVjBMg-=(gNtd;6IaNtn7722qH7R@-l@}qtM zdz?p|{Zs%+s5${W%HWS#fh#jry%;IiGhz7vVuUvj3{VT)--tv?$l)+ie% zHib-}W<1$E**%ltPGK$BF;-)g35YB8e^$ZN0MmM$KteaLdpN)EZ-^eYvMI^&)AoF; z;mdGt#$aWEjc;OC&0rJ8+A#irLpE_oKEJV=y9JDPUa8?HC^h8OsqJcF_ow|6dWMl= z6#`b<3foHCD%(+JbSG=ZUsum9DVY8~w%ogLX(`yh-V(OkYfRR1jFEeLq_F*L`vutA zE(J}8!~tm&WU_wORvZm!2Y#fIW$Ey!Rc1(wu>noXovK6RaZR`(>xP9 zT{|cT*V$&m)W9Xv|D+lZt^I7l!YKYzC~*HDHJak?mCo%Xe}iE}p0ls2Ow2;d-})+&0hBRjA+cVc}%Veh}Hyw`vvt%?cGV z1O;$JAKo-Q_5g^cIIw>hr!00%G;!oSch@<&KbDEpazv$h)jfdb?FPIA;17h2S3k-6|%ls7sw@0uq6O~%I zlLO__VXine}z5%wbmpP!HQ>AR=;oGoO@lsH`vI2Wg3 zmuH3~xCvO+u6`9B+p=F=wQ|{W9ka@ZsQk5ggm=WMW|2sLo@XToG>^p!P#~!y$x8eg zl;Z$GjT6{lh19#YgxUl#KE!IY(StBCFf)hKjqr|_y^KR%)*md(bXm)^OM-B@{-oyw z76ThBMB^j$G3^02U-e1HBjeO{Pt9V$1qAB_&Vn2i7|d-@Y%hcpawgAmEZ?%)xz68} ze*PD~@dG%>Z79B2en7QE%M=D(wJjDf7_=$ke}&YMN)!Bd)uj1Bp5i*V92DLtt6v2! zZzoyBV*(BFtMM6@vOj$28T#;OSGP@si;|0uiIYXiqifdkDwh#y#Y0oUx%<*~;6tG_7KMtRd*efMFdQ=2x= z#Cr_1I%Z8)!!(@w6igj$tNz9Mj9MT>Ol@HH89X=CXHtKYa}m$Y39LSo$u2Ba0Gdw_ zbKb~3%K(3mZ0pAF2bXhrr?bZuMbX9qji`%!o8lb(DOl&{Ut8=KJHI1~tPKKB_vI&N z_7R4T87Z18)_+VisTdCI@2om4Ng1!vftAQW7QR4QG8%?2#ftRzu@@yvufVzvp?2|< zs;;kD`74uf!+66q!(77>LwC!hXXD%Qu`V>XkaA4^i@HkrmP7u*J?(2hSMROqi?KpB zWx#1&?A8ueQP(H7^RuCWTO9@(x&t zalW!+rY^#x((wUU37mjZvaZhLn{P84CMO7Q{NB{06kmV2KdhmCk*Y~bL=L~qi!NU!WLDbf8@d&?^m zCPr{Ux>+L_G4(bF;yJ9?v)OtSbwoV-?D0%qg=+cBa_#a|9HfTieZ0x%l7bq%bGAM! z;+&af?<~F;^i~@fkM;hKBA+M$Ux{27P+B{)VC0G@E%a~mA2n!#eY|~wJ=+Biy5~NS z9c@Sy!4DTls&0b)R%rZ@M&=QDAT~M1yQzV1N(t7cuwbfE({8^q{S5%<|ru|2_wLeb)8YK0!{B>E(cjR2t8O z_!;`X`Hb_G|1tLt=kSME9@B5H|MYdvjp1#m*xQ2qy+clXC#kt03_x{Tz3u{%Z0;)m<3%BFBqnkOE2n=cH7g$lXacFn+sx0 z+>7tK`~%3L)rXREgw7qKNb##KD^%)j&ZV5B$_Aalp_|5!VU%ZqACSMHm!a(C}mDLN#eO08!ro_$HxZh-> z6{&q{G9rSb$)`?TWLLE)m}aRsX_`mxT3G|WV67b-LlT+4=46Jx=gIG}YnDvd@F2mhJ!EZFVbr30&ZvjAl|U8_we}3D-~~ zb19ksMIygJVL716?yLAPe{h-}QPBX?9N}n-5P*t|L?WeseXlaVGBBgao_h-6?a>2_ zx^O6ylnt)k?l_H~GD$55#;VG^Mf?CQ{5nt>_B%XI?)TL4` zbKe9xSGWPbFAdcet8`A>qBI-~mSYqR@8UB`f;o zokN)$hdk^5u_ks~w!-xu(lJA? z!S2Ua#|lAryI;s2zm9Q5-3@_sI8khfi+|%AeX{RBzx#CIkE#yzn<^L~PeG-u|AT2QIUboyf+I9PGQ#vgc(e zSnq@wi3o%{1vA8y?^{dI?l3SGmoSBiVszN#SpQSsR{ih-nF;P+_{p+2)ru=ZmvvKj zA15cgPY6wjOt>A-8oX%A2xQ~u8AvEY6F#5H=xS1pfr%&qut^FU1Rvh}{$&C?0BdNd zUI6kPp8i)FQVKYWzQK-43o>9t>9xa1sM9W_vdKXJU|0Ao$lHMU7xP1b-PL?Kz_psx z#j_IJm}KwGAEZ!}(PQDVT0Jxl8RBj9o+F~!tn-LSTiIVSu4a@)-F1hX?io3dKZZgZ zhm;>T7U?M2&d8*;XRh6tpVjJ4JXeSQzaF@2niaQE0%kMl?S{fHV`I0c$e!-6iskX< zspL82MdX#{4FVDe*8Z;_z`$6oOid;`oAKC5Vnbdfd7?S25)x8-{OQjY`mRJtINNT9 zsRE5s0xRmkBt7Z2DbG6Kahn`}mP3Nzi}*yNXRE?lTw-X#ZG32lF&G^&{XgoHBL*(c zl?TptTbnHIqZt^J+Cc^YKW-0~$%@VD zhaGqe405NisdUvRA2ImarrdnOVZ@u~op((1$X%ye-%01nq+jMpO{1cN>J!S<5g|b`W&vuv7SbHp?{^S&3_pdN?G|ly_-vUaXpCyl839bLr4`n|ES3- z-=InSwT^4oY90Ck4>mx6onO? z5FkDOLPQ-eDW>e| zwDZ#HUn+>S2P=U4PLZTjlg}EdffkPTYj75ARDjNymf5k_aXq&`TfQhBccM56=D)eY zlm>9h|5^-~VJ+6@s-bRw5_gK-Q#0+43PsAEgQ@dqS<@7(S_8f2&0t1cBF86zejxr@ z#EM|nbK_@cHH@jQ9Qpfks0pz=Ug}WL$GjTZaZUMmYTac~;#JGdC_mk@zK8507bs;|Th8+U37StYq z=P3Tq$0qdR0LvzxDl(A9psN>AZ6PlU&Zz3XnFk~LWh&s&i{4yOv8ES2ymaQ;Q6o?V z;V+_Lf0*8U`o78uavY3PY1X(XYyRv??p))QOETi!*?|||_xc%ckmU+?RsbR z;v5$cRrwD+grW)lq3FI1AjSy&tHRi?LJlh5kaPyXO*X_`^xSA-Wa4;HupAlWywt9XtX88G0P6sL-cC%ju`G|z!{6Puyr z27uu(wik+}aVb{Dy_a350p~!MVre~F8Wb>Px9+}ls?e{w>vDe^dfNQkKgymoW_&$vV78|~XTK>Xs$_|QfW0~%pViS0yb`WsVh7DPHw?1j0x7&a0oPlLP?y6*ql3cr4 zF{NrN?&1Th?`_y2N;jk*<7(5dKi@8d#gTA3JmTZc5SV8 zZ_7yUclCXc)$Cl=1rJpfe<5XVT$LAV_Z%AId8yufbWNU{J@Iul9T!02z}XdVNJ>ld zLMo2h@W}7kwa2y8h+SPv2H7r2FicDodE;&H}P~0K5`bL}e_nb={ zz_8WWE!k>_tiRpHdT4SM_DPYZ6%DAd$PHUgQ6+9RubLFXYc_QsSg19B%>T31b-{=P zWbUB2cQ7*`C2hz&fv;2vSSP^=$S?!dWE|SVU>kiqKJO#U=KRDzfUZZ|_@B2`7B@Xr zpbk&3`-;x@^ybrd{edsJ*3Ga11?OL`GxM#(e>#Dv2eF{#=ZPgIuAdAadf7g{R(*V`N}gQuh=-T~Fy#mpqwt60u`OUTJr}Uw z`=vakmU#L$k{KJXqQ7Ue_V6b=G?M|yK=``CZY+&u>-k>96CHLyFC4M4L&s=$@X4o> zCs}GPVD5)K1bF?d=iYzhr6%+4y{|jdq8~XQ(A-Qz9F7jH*%@vB!}=Bi7_n8oFpv5V z%g>>O%AOL*yWEDUxoR7oJzOViGvuED8SR_*=q=#W3Lr-@zSYukWn$6YGoS8-ld8pL-GGjPh<{8MZwd%DgiU8}b=AKqykoSMZ z@Z1H0kdtIB0w4pAf!(irt&h!*o3T~dzu3~nI5dFqJ6MJOxqARn zuzyoWy)19St-Vt%jMDX5XV5-+;r#vC>a$J(-Bhr@q5;;s zZThh(ELCj2MWT@d-{C+3wQQmFlM=Bij6{8q3Zlq7qw(%Kc!Exf?ZGqi(5^e7e3^v2 z-L|i(I3ODA!o;Vu5!h*B^@Ub2QqFbpJ}9T(Apk_RC?wz?6nJk0cXy>gtGvip(1#v; zTcxTkC(A}zbpA7hGD3w9AKbkKxDdd-U}d(H%7f>&4qS0ihTM`6s8YV2z4`nLJ>1GS zYkJ8Z_g?X(kvo8cM(qw>+0V`IM?bFG(3fQ+*C#rmT+eq^`AwnOaQv7Y!1+(&#sQra zYL=WtK-$`O))pU?|CH1h0M?UXsO@E!(=zJJfWLE1SWNv;T-%q|M%dKvc+ZS%*c;}2 zu=H5xQM-F%=^p)THNbk)#9zT0O|D(v@^?~BZw4`@8+%v!)-V}v zz!f$YO$SZ|bT9}YWG$lC#{1x@`TOS8@0G5HJ>3>Hv2yvf?pFmyX(pJt@d+KQ^&O9= ze&FDI+Jmh$r2Ct7{lg^iP0zqcpo#ehV*g&z2MqicqSfyv>Zr3?k0U*0nk@Q?4zL>j zZY-k$sXK)dApJ4Ukvw-rPJ@nt$JzHNRs#M)p_KFJ8@9E-J(wp$s%5K%5wmI}pLx<+ z(&@!xs>uX=Q`EoAw=26|cW?W4RfIsL*$;-}iftX3ew!f8%)HBFA{}_8TTZM$xi&Ld&l1e_UZ3$tnd58!4j0T6`@|Ob*_7V?%G@zC=L5*^ymBD29~L|7H2H3 zUtX`uU``*gUvOMqOFYnTP_wa# z)CbtP86G8#@R&P_@6-VcRshk{w+K(4 zuf06Nyttv|^hLZ6hfayS=w53?CaOZlbL3B=jmg~4+T5+=fk*uWKX#v;zkT%Xp=cVGb{v)S72`5(5J(T)Y6__4m?a z0c|Crd2IJp>#48sdW8x5#X(fko-tco6mhp}v*~4jiCPned_O8z;rYv3))ItJ;D~E} zl|yd>o2=`+^ld-$EA@=BKW+>QWLCZ5BHxXV=v@H=(YK?M=<9j^)$YoP^<6Nd;Hs%d zUFDUx&Hv!-j;@K;-b9nwUYvv-xbm)AY3kSRR~XDm&b5iM$Ky&k(Ox_^L5fqd)=N` zAwjRM{xPc_0dfP?3EXZlvNri`=XR1xbm@#<2kl%IGWen9vB$X*#*2T^sS5_FTZmEc zIN9ZXxJ8Dkjy{M{KaVC^v8)dDlNU|ujbAkxPu_XOcY8f1cWx;!Dl)sPGwP7z)nTwqyH(OoG7 z+rR80W_!AXxh$O$7%`^2*gBGNfCC2?n27JCBBmi*q_cm~+ka;X<>dK|_$OZe5!~G5 zZRY?FWW2M~IeQ*#Ql4uUwdMJrQe#|MwVLy;#0UnTlRA)x@E ztN1Iq-DE1b%?a8@hB8T5uE4>PT~e0r95B39*HxY&(HV-+R%FSJN2W!INhrfqqGe6h zX!GPjf_fYk;)}ucT4^_SJNG>BB|dhpT|oKDT(~b7rTKgWl3MtG=nn~bEhzZl*bMlUSf&5l~ zmTc$iquH^ZGU6_!XL7pyoEbd0%)zT~hq@x!xDql&v@7LzpN%?Tsj)2tlc=Y}? zEBcaItXCc*7~F;+;0)vo%zfxhU9vs8Io{1PeBFDyC(CWK@56ezqa zK>JagzUDGfjo@eh%!(~Lb4=ThXHS6z_7P42!V6StK2DIf&emmOKjdAj?de66h~KCT zEaF;Qv2Gx-`N^3IUU_ov$DSy$-%r~Ni*!R?XwBNCK6q5r$4T&(H(259UJ6V99zJB)}IIm(*Nq|rpC3@ynl zxZ31*mP*pgFzW{Wy*a)ze3P>rQ{DGJS~DB9C5fPaU}xSE6y0ZxGiQ>TMl*;RaHoU; z!$0}jZN_D@0?I5SgU%dmRhnySw8}{wmQHqCButjRdTp#HPw)F&HZb-p1~y@FVCZI6 z*z7Cqs|Bonn^@24kk^SVb$}-Oq(X8!21SX=Z@g}nu5%IfJuZ0ED*qN}w{pj6{^a}O zAxE8Rzq+TXb1SCnU43s%t|a1Q2EiIhfhI|v-7)zjmXw_BEI-uQFD|@-5lUh|2F`M{ z%18%n6ro9(ULBpbJ8qP~zM?g$Z|gp@gHzC-4guwC5j|ux)o`F{ zQRTv?Uo&mYk))~krt3I^n3_u12*zA1m*bIb<7uR&|4{uEUA~gz?6FjqB)v>(br5j!!*lm(4^>$FREu5td!4y<_!AGL6QPES(8(>1iEiOC>$&^X5-hHv``{ z;8M3; u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) +} + +impl Activity { + pub fn new() -> Arc { + let a = Arc::new(Self::default()); + a.touch(); + a + } + + pub fn touch(&self) { + self.0.store(now_secs(), Ordering::Relaxed); + } + + pub fn idle(&self) -> Duration { + Duration::from_secs(now_secs().saturating_sub(self.0.load(Ordering::Relaxed))) + } +} + +/// Pure decision for one tick, unit-tested below. +pub fn decide(idle: Duration, limit: Duration, screen_locked: bool, wall_gap: Duration) -> Option { + if wall_gap > POLL + SLEEP_GAP { + Some(Reason::Sleep) + } else if screen_locked { + Some(Reason::ScreenLocked) + } else if idle >= limit { + Some(Reason::Idle) + } else { + None + } +} + +/// Polls in a background thread. `is_unlocked` gates the checks; `lock` is called on a trigger. +pub fn spawn( + activity: Arc, + limit: impl Fn() -> Duration + Send + 'static, + is_unlocked: impl Fn() -> bool + Send + 'static, + lock: impl Fn(Reason) + Send + 'static, +) { + thread::Builder::new() + .name("autolock".into()) + .spawn(move || { + let mut last_wall = SystemTime::now(); + let mut last_mono = Instant::now(); + loop { + thread::sleep(POLL); + let wall = SystemTime::now(); + // Monotonic clocks may pause during sleep, the wall clock doesn't. + let gap = wall.duration_since(last_wall).unwrap_or_default().max(last_mono.elapsed()); + last_wall = wall; + last_mono = Instant::now(); + if !is_unlocked() { + continue; + } + let idle = platform::system_idle().unwrap_or_else(|| activity.idle()); + if let Some(r) = decide(idle, limit(), platform::screen_locked(), gap) { + lock(r); + } + } + }) + .expect("spawn autolock thread"); +} + +#[cfg(target_os = "macos")] +mod platform { + use std::time::Duration; + + use core_foundation::base::{CFType, TCFType}; + use core_foundation::boolean::CFBoolean; + use core_foundation::dictionary::{CFDictionary, CFDictionaryRef}; + use core_foundation::string::CFString; + + #[link(name = "CoreGraphics", kind = "framework")] + extern "C" { + fn CGEventSourceSecondsSinceLastEventType(state: i32, event_type: u32) -> f64; + fn CGSessionCopyCurrentDictionary() -> CFDictionaryRef; + } + + const HID_SYSTEM_STATE: i32 = 1; + const ANY_INPUT_EVENT: u32 = u32::MAX; + + pub fn system_idle() -> Option { + // SAFETY: pure query with constant arguments. + let s = unsafe { CGEventSourceSecondsSinceLastEventType(HID_SYSTEM_STATE, ANY_INPUT_EVENT) }; + (s.is_finite() && s >= 0.0).then(|| Duration::from_secs_f64(s)) + } + + pub fn screen_locked() -> bool { + // SAFETY: a Copy function: we own the returned dictionary (create rule), or get null. + let raw = unsafe { CGSessionCopyCurrentDictionary() }; + if raw.is_null() { + return false; + } + // SAFETY: raw is a non-null CFDictionary we own. + let dict: CFDictionary = unsafe { CFDictionary::wrap_under_create_rule(raw) }; + dict.find(CFString::from_static_string("CGSSessionScreenIsLocked")) + .and_then(|v| v.downcast::()) + .is_some_and(bool::from) + } +} + +#[cfg(windows)] +mod platform { + use std::time::Duration; + + use windows_sys::Win32::System::StationsAndDesktops::{CloseDesktop, OpenInputDesktop, SwitchDesktop, DESKTOP_SWITCHDESKTOP}; + use windows_sys::Win32::System::SystemInformation::GetTickCount; + use windows_sys::Win32::UI::Input::KeyboardAndMouse::{GetLastInputInfo, LASTINPUTINFO}; + + pub fn system_idle() -> Option { + let mut info = LASTINPUTINFO { cbSize: std::mem::size_of::() as u32, dwTime: 0 }; + // SAFETY: info is a valid, sized LASTINPUTINFO. + if unsafe { GetLastInputInfo(&mut info) } == 0 { + return None; + } + // SAFETY: no preconditions. Both are u32 ms tick counts; wrapping_sub handles the 49-day wrap. + let now = unsafe { GetTickCount() }; + Some(Duration::from_millis(u64::from(now.wrapping_sub(info.dwTime)))) + } + + /// While the workstation is locked the input desktop is Winlogon's, which we can't switch to. + pub fn screen_locked() -> bool { + // SAFETY: the desktop handle is closed before returning. + unsafe { + let h = OpenInputDesktop(0, 0, DESKTOP_SWITCHDESKTOP); + if h.is_null() { + return true; + } + let switchable = SwitchDesktop(h) != 0; + CloseDesktop(h); + !switchable + } + } +} + +#[cfg(all(unix, not(target_os = "macos")))] +mod platform { + use std::process::Command; + use std::time::Duration; + + pub fn system_idle() -> Option { + None + } + + pub fn screen_locked() -> bool { + let session = std::env::var("XDG_SESSION_ID").unwrap_or_else(|_| "auto".into()); + Command::new("loginctl") + .args(["show-session", &session, "-p", "LockedHint", "--value"]) + .output() + .ok() + .filter(|o| o.status.success()) + .is_some_and(|o| o.stdout.trim_ascii() == b"yes") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decisions() { + let min = Duration::from_secs(60); + let tick = POLL; + assert_eq!(decide(Duration::ZERO, 10 * min, false, tick), None); + assert_eq!(decide(10 * min, 10 * min, false, tick), Some(Reason::Idle)); + assert_eq!(decide(Duration::ZERO, 10 * min, true, tick), Some(Reason::ScreenLocked)); + assert_eq!(decide(Duration::ZERO, 10 * min, false, 5 * min), Some(Reason::Sleep)); + } + + #[test] + fn platform_probes_do_not_panic() { + let _ = platform::system_idle(); + let _ = platform::screen_locked(); + } + + #[test] + fn activity_tracks_touch() { + let a = Activity::new(); + assert!(a.idle() < Duration::from_secs(2)); + } +} diff --git a/src-tauri/src/bin/kestrove-nmh.rs b/src-tauri/src/bin/kestrove-nmh.rs index 16cd383..634b050 100644 --- a/src-tauri/src/bin/kestrove-nmh.rs +++ b/src-tauri/src/bin/kestrove-nmh.rs @@ -1,54 +1,65 @@ -//! Native messaging host for Chrome/Edge/Firefox (kestrove-spec/docs/05). +//! Native messaging host for Chrome/Edge/Brave/Firefox (kestrove-spec/docs/05). //! -//! The browser starts this process and talks over stdio: each message is a u32 length -//! (native byte order) followed by UTF-8 JSON, at most 1 MiB to the browser. -//! This process holds **no** secrets. It relays each request to the running desktop app over -//! a local IPC channel (Windows named pipe `\\.\pipe\kestrove-nmh-`, Unix socket -//! `$XDG_RUNTIME_DIR/kestrove/nmh.sock` with 0600 perms). The app re-checks origins and asks for -//! approval or unlock as needed. +//! The browser starts this process and talks over stdio: each message is a u32 length (native +//! byte order) followed by UTF-8 JSON. This process holds **no** secrets and can't read the +//! traffic: frames are end-to-end encrypted between the extension and the app (pairing channel, +//! see `channel.rs`). It relays each frame to the running app over local IPC (`ipc.rs`) and +//! relays the app's reply back, strictly one reply per request. +//! +//! When the app isn't running, every request gets `{t:"error", code:"unavailable"}`; it +//! reconnects on the next request, so starting the app later just works. -use std::io::{self, Read, Write}; +use std::io::{self, Write}; -use serde_json::{json, Value}; +use kestrove_desktop::framing::{read_frame, read_json, write_frame, write_json}; +use kestrove_desktop::ipc::{self, Conn}; +use serde_json::json; -const MAX_IN: usize = 4 * 1024 * 1024; - -fn read_message(stdin: &mut impl Read) -> io::Result> { - let mut len = [0u8; 4]; - match stdin.read_exact(&mut len) { - Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), // browser closed the port - Err(e) => return Err(e), +/// Chrome passes the caller origin (`chrome-extension:///`) as the first argument; Firefox +/// passes the manifest path and then the extension id. Informational only (shown when pairing): +/// the browser's `allowed_origins` check and the pairing key are what actually gate access. +fn caller() -> String { + let args: Vec = std::env::args().skip(1).collect(); + match args.as_slice() { + [a, ..] if a.starts_with("chrome-extension://") => a.clone(), + [_, id, ..] if !id.starts_with("--") => id.clone(), + _ => String::new(), } - let len = u32::from_ne_bytes(len) as usize; - if len > MAX_IN { - return Err(io::Error::new(io::ErrorKind::InvalidData, "message too large")); - } - let mut buf = vec![0u8; len]; - stdin.read_exact(&mut buf)?; - serde_json::from_slice(&buf).map(Some).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } -fn write_message(stdout: &mut impl Write, v: &Value) -> io::Result<()> { - let bytes = serde_json::to_vec(v)?; - stdout.write_all(&(bytes.len() as u32).to_ne_bytes())?; - stdout.write_all(&bytes)?; - stdout.flush() +fn connect(caller: &str) -> io::Result { + let mut c = ipc::connect()?; + write_json(&mut c, &json!({ "t": "nmh_hello", "caller": caller }))?; + match read_json(&mut c)? { + Some(v) if v["t"] == "ok" => Ok(c), + _ => Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected hello reply")), + } } -/// TODO(M3): forward to the desktop app over local IPC, inside the paired AEAD channel. -fn relay(req: &Value) -> Value { - match req.get("type").and_then(Value::as_str) { - Some("hello") => json!({ "type": "hello", "version": env!("CARGO_PKG_VERSION"), "paired": false }), - Some(_) => json!({ "type": "error", "code": "unavailable" }), - None => json!({ "type": "error", "code": "bad_request" }), - } +fn relay(conn: &mut Conn, frame: &[u8]) -> io::Result> { + write_frame(conn, frame)?; + read_frame(conn)?.ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "app closed the connection")) } fn main() -> io::Result<()> { + let caller = caller(); let (mut stdin, mut stdout) = (io::stdin().lock(), io::stdout().lock()); - while let Some(req) = read_message(&mut stdin)? { - write_message(&mut stdout, &relay(&req))?; + let unavailable = serde_json::to_vec(&json!({ "t": "error", "code": "unavailable" }))?; + let mut conn: Option = None; + while let Some(frame) = read_frame(&mut stdin)? { + if conn.is_none() { + conn = connect(&caller).ok(); + } + let reply = match conn.as_mut().map(|c| relay(c, &frame)) { + Some(Ok(r)) => r, + Some(Err(_)) => { + conn = None; + unavailable.clone() + } + None => unavailable.clone(), + }; + write_frame(&mut stdout, &reply)?; + stdout.flush()?; } Ok(()) } diff --git a/src-tauri/src/broker.rs b/src-tauri/src/broker.rs new file mode 100644 index 0000000..5bed48b --- /dev/null +++ b/src-tauri/src/broker.rs @@ -0,0 +1,705 @@ +//! The app's side of the browser protocol: pairing, encrypted sessions and request handling. +//! +//! Outer frames (JSON, field `t`), relayed unchanged by `kestrove-nmh`: +//! +//! | extension → app | app → extension | +//! |---|---| +//! | `{t:"nmh_hello", caller}` (sent by the host itself) | `{t:"ok"}` | +//! | `{t:"ping"}` | `{t:"pong", version}` | +//! | `{t:"pair_init", pub, name}` | `{t:"pair_started", pairing_id, pub}` (app shows the words) | +//! | `{t:"pair_confirm", pairing_id, proof}` | `{t:"pair_waiting"}` / `{t:"pair_done"}` / error | +//! | `{t:"open", pairing_id, nonce}` | `{t:"opened", nonce}` / `{t:"error", code:"not_paired"}` | +//! | `{t:"msg", n, c}` | `{t:"msg", n, c}` / `{t:"error", code:"bad_session"}` | +//! +//! `msg` plaintexts are `{rid, body}` where `body` is a `NativeRequest` / `NativeResponse` from +//! kestrove-browser/src/protocol.ts. Origins come from the extension's `sender`, and are +//! re-checked here against the vault before any secret leaves the app. + +use std::io::{self, Read, Write}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use base64::engine::general_purpose::STANDARD as B64; +use base64::Engine; +use kv_ffi::{CoreError, ItemDto, ItemKindDto, VaultCore}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use zeroize::Zeroizing; + +use crate::channel::{random, verify_confirm, Ephemeral, Session}; +use crate::framing::{read_frame, write_json}; +use crate::private_file; + +/// How long a pairing request waits for the user in the app. +const PAIRING_TTL: Duration = Duration::from_secs(300); +/// How long a save prompt stays in the app. +const SAVE_TTL: Duration = Duration::from_secs(600); +const MAX_PENDING_SAVES: usize = 8; + +/// What the app UI must show. Implemented with Tauri events in the app, and auto-approving in tests. +pub trait Ui: Send + Sync { + fn pairing_requested(&self, req: &PairingView); + fn save_requested(&self, req: &SaveView); + /// A user-driven request (a fill) arrived: counts as activity for auto-lock. + fn activity(&self) {} +} + +#[derive(Debug, Clone, Serialize)] +pub struct PairingView { + pub id: String, + pub name: String, + pub caller: String, + pub words: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct SaveView { + pub id: String, + pub origin: String, + pub username: String, + /// Some item already has this username for this site: saving updates its password. + pub update: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredPairing { + pub id: String, + pub name: String, + pub caller: String, + pub words: Vec, + pub created_at: u64, + key: String, +} + +impl StoredPairing { + fn key(&self) -> Option> { + let raw = Zeroizing::new(B64.decode(&self.key).ok()?); + let arr: [u8; 32] = raw.as_slice().try_into().ok()?; + Some(Zeroizing::new(arr)) + } +} + +/// Persisted pairings (`pairings.json`, 0600). The pairing key only authenticates the +/// extension to the app; it can't decrypt the vault. Every secret still needs an unlocked vault. +pub struct PairingStore { + path: Option, + items: Mutex>, +} + +impl PairingStore { + pub fn in_memory() -> Self { + Self { path: None, items: Mutex::new(Vec::new()) } + } + + pub fn open(path: PathBuf) -> io::Result { + let items = match private_file::read(&path)? { + Some(b) => serde_json::from_slice(&b).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?, + None => Vec::new(), + }; + Ok(Self { path: Some(path), items: Mutex::new(items) }) + } + + fn guard(&self) -> MutexGuard<'_, Vec> { + self.items.lock().unwrap_or_else(|e| e.into_inner()) + } + + fn persist(&self, items: &[StoredPairing]) -> io::Result<()> { + match &self.path { + Some(path) => private_file::write(path, &serde_json::to_vec_pretty(items)?), + None => Ok(()), + } + } + + pub fn list(&self) -> Vec { + self.guard().clone() + } + + fn get(&self, id: &str) -> Option { + self.guard().iter().find(|p| p.id == id).cloned() + } + + fn add(&self, p: StoredPairing) -> io::Result<()> { + let mut items = self.guard(); + items.push(p); + self.persist(&items) + } + + pub fn remove(&self, id: &str) -> io::Result { + let mut items = self.guard(); + let before = items.len(); + items.retain(|p| p.id != id); + self.persist(&items)?; + Ok(items.len() != before) + } +} + +struct PendingPairing { + view: PairingView, + key: Zeroizing<[u8; 32]>, + at: Instant, +} + +struct PendingSave { + id: String, + origin: String, + username: String, + password: Zeroizing, + at: Instant, +} + +pub struct Broker { + core: Arc, + ui: Arc, + pairings: PairingStore, + pending_pairing: Mutex>, + saves: Mutex>, +} + +fn lock(m: &Mutex) -> MutexGuard<'_, T> { + m.lock().unwrap_or_else(|e| e.into_inner()) +} + +fn now_secs() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) +} + +fn err(code: &str) -> Value { + json!({ "type": "error", "code": code }) +} + +fn core_err(e: &CoreError) -> Value { + match e { + CoreError::Locked | CoreError::NoAccount => err("locked"), + CoreError::NotFound => err("origin_mismatch"), + _ => err("internal"), + } +} + +fn b64_array(v: &Value) -> Option<[u8; N]> { + B64.decode(v.as_str()?).ok()?.try_into().ok() +} + +/// `scheme://host[:port]` for http(s) origins; anything else (paths, credentials, other schemes) is rejected. +pub fn normalize_origin(raw: &str) -> Option { + let u = url::Url::parse(raw).ok()?; + if !matches!(u.scheme(), "https" | "http") || u.host_str().is_none_or(str::is_empty) { + return None; + } + if !u.username().is_empty() || u.password().is_some() { + return None; + } + Some(u.origin().ascii_serialization()) +} + +/// Per-connection state. +pub struct Conn { + caller: String, + session: Option, +} + +impl Broker { + pub fn new(core: Arc, ui: Arc, pairings: PairingStore) -> Arc { + Arc::new(Self { core, ui, pairings, pending_pairing: Mutex::new(None), saves: Mutex::new(Vec::new()) }) + } + + pub fn core(&self) -> &Arc { + &self.core + } + + /// Serves one `kestrove-nmh` connection until it closes. Strict request → response. + pub fn serve(&self, mut io: impl Read + Write) -> io::Result<()> { + let mut conn = Conn { caller: String::new(), session: None }; + while let Some(frame) = read_frame(&mut io)? { + let reply = match serde_json::from_slice::(&frame) { + Ok(v) => self.handle_frame(&mut conn, &v), + Err(_) => json!({ "t": "error", "code": "bad_request" }), + }; + write_json(&mut io, &reply)?; + } + Ok(()) + } + + pub fn handle_frame(&self, conn: &mut Conn, v: &Value) -> Value { + let outer_err = |code: &str| json!({ "t": "error", "code": code }); + match v.get("t").and_then(Value::as_str) { + Some("nmh_hello") => { + conn.caller = v.get("caller").and_then(Value::as_str).unwrap_or("").chars().take(200).collect(); + json!({ "t": "ok" }) + } + Some("ping") => json!({ "t": "pong", "version": env!("CARGO_PKG_VERSION") }), + Some("pair_init") => { + let Some(ext_pub) = v.get("pub").and_then(b64_array::<32>) else { return outer_err("bad_request") }; + let name: String = v.get("name").and_then(Value::as_str).unwrap_or("Browser").chars().take(80).collect(); + self.pair_init(conn, &ext_pub, name).unwrap_or_else(|| outer_err("bad_request")) + } + Some("pair_confirm") => { + let (Some(id), Some(proof)) = ( + v.get("pairing_id").and_then(Value::as_str), + v.get("proof").and_then(Value::as_str).and_then(|p| B64.decode(p).ok()), + ) else { + return outer_err("bad_request"); + }; + self.pair_confirm(id, &proof) + } + Some("open") => { + let (Some(id), Some(ne)) = + (v.get("pairing_id").and_then(Value::as_str), v.get("nonce").and_then(b64_array::<16>)) + else { + return outer_err("bad_request"); + }; + let Some(key) = self.pairings.get(id).and_then(|p| p.key()) else { return outer_err("not_paired") }; + let na = random::<16>(); + conn.session = Some(Session::app(&key, &ne, &na)); + json!({ "t": "opened", "nonce": B64.encode(na) }) + } + Some("msg") => { + let Some(session) = conn.session.as_mut() else { return outer_err("bad_session") }; + let (Some(n), Some(c)) = ( + v.get("n").and_then(Value::as_u64), + v.get("c").and_then(Value::as_str).and_then(|c| B64.decode(c).ok()), + ) else { + return outer_err("bad_request"); + }; + let Ok(pt) = session.open(n, &c) else { + conn.session = None; // the extension re-opens a fresh session + return outer_err("bad_session"); + }; + let parsed: Option = serde_json::from_slice(&pt).ok(); + drop(pt); + let (rid, body) = match &parsed { + Some(p) => (p.get("rid").cloned().unwrap_or(Value::Null), p.get("body").cloned().unwrap_or(Value::Null)), + None => (Value::Null, Value::Null), + }; + let reply = Zeroizing::new( + serde_json::to_vec(&json!({ "rid": rid, "body": self.handle_request(&body) })).unwrap_or_default(), + ); + let session = conn.session.as_mut().expect("session still open"); + let (n, c) = session.seal(&reply); + json!({ "t": "msg", "n": n, "c": B64.encode(c) }) + } + _ => outer_err("unknown_type"), + } + } + + // ------------------------------------------------------------ pairing + + fn pair_init(&self, conn: &Conn, ext_pub: &[u8; 32], name: String) -> Option { + let eph = Ephemeral::generate(); + let p = eph.pair(ext_pub)?; + let view = PairingView { + id: uuid::Uuid::new_v4().to_string(), + name, + caller: conn.caller.clone(), + words: p.words.iter().map(|w| w.to_string()).collect(), + }; + let reply = json!({ "t": "pair_started", "pairing_id": view.id, "pub": B64.encode(eph.public) }); + self.ui.pairing_requested(&view); + // One pending request at a time; a new one replaces the old (and its words). + *lock(&self.pending_pairing) = Some(PendingPairing { view, key: p.key, at: Instant::now() }); + Some(reply) + } + + fn pair_confirm(&self, id: &str, proof: &[u8]) -> Value { + if let Some(key) = self.pairings.get(id).and_then(|p| p.key()) { + return if verify_confirm(&key, id, proof) { + json!({ "t": "pair_done" }) + } else { + json!({ "t": "error", "code": "not_paired" }) + }; + } + match lock(&self.pending_pairing).as_ref() { + Some(p) if p.view.id == id && p.at.elapsed() < PAIRING_TTL && verify_confirm(&p.key, id, proof) => { + json!({ "t": "pair_waiting" }) + } + _ => json!({ "t": "error", "code": "not_paired" }), + } + } + + pub fn pending_pairing(&self) -> Option { + let mut g = lock(&self.pending_pairing); + if g.as_ref().is_some_and(|p| p.at.elapsed() >= PAIRING_TTL) { + *g = None; + } + g.as_ref().map(|p| p.view.clone()) + } + + /// The user compared the words and accepted (or rejected) the pairing. Requires an unlocked + /// vault, so someone at a locked machine can't pair a new extension. + pub fn resolve_pairing(&self, id: &str, accept: bool) -> Result<(), String> { + let mut g = lock(&self.pending_pairing); + let Some(p) = g.take_if(|p| p.view.id == id) else { return Err("This pairing request expired.".into()) }; + if !accept { + return Ok(()); + } + if self.core.is_locked() { + *g = Some(p); + return Err("Unlock Kestrove to pair a browser.".into()); + } + if p.at.elapsed() >= PAIRING_TTL { + return Err("This pairing request expired.".into()); + } + self.pairings + .add(StoredPairing { + id: p.view.id, + name: p.view.name, + caller: p.view.caller, + words: p.view.words, + created_at: now_secs(), + key: B64.encode(p.key.as_ref()), + }) + .map_err(|e| format!("Could not save the pairing: {e}")) + } + + pub fn pairings(&self) -> Vec { + self.pairings.list() + } + + pub fn revoke_pairing(&self, id: &str) -> Result<(), String> { + self.pairings.remove(id).map(|_| ()).map_err(|e| e.to_string()) + } + + // ------------------------------------------------------------ requests + + pub fn handle_request(&self, body: &Value) -> Value { + let Some(kind) = body.get("type").and_then(Value::as_str) else { return err("bad_request") }; + if kind == "hello" { + return json!({ "type": "hello", "version": env!("CARGO_PKG_VERSION"), "paired": true }); + } + let Some(origin) = body.get("origin").and_then(Value::as_str).and_then(normalize_origin) else { + return err("bad_request"); + }; + let item_id = || body.get("itemId").and_then(Value::as_str).filter(|s| !s.is_empty()); + match kind { + "query" => match self.core.items_for_url(origin) { + Ok(items) => json!({ + "type": "identities", + "items": items.iter().map(|i| json!({ + "id": i.id, "title": i.title, "username": i.username.clone().unwrap_or_default(), "totp": i.has_totp, + })).collect::>(), + }), + Err(e) => core_err(&e), + }, + "fill" => { + let Some(id) = item_id() else { return err("bad_request") }; + self.ui.activity(); + match self.matching_item(&origin, id) { + Ok(d) => json!({ + "type": "credential", + "username": d.username.clone().unwrap_or_default(), + "password": d.password.clone().unwrap_or_default(), + }), + Err(v) => v, + } + } + "totp" => { + let Some(id) = item_id() else { return err("bad_request") }; + self.ui.activity(); + match self.matching_item(&origin, id) { + Ok(d) => match d.totp.as_deref().map(|t| kv_ffi::totp_code(t.to_string(), now_secs())) { + Some(Ok(c)) => json!({ "type": "totp", "code": c.code, "secondsRemaining": c.seconds_remaining }), + _ => err("bad_request"), + }, + Err(v) => v, + } + } + "save" => { + let username = body.get("username").and_then(Value::as_str).unwrap_or(""); + let Some(password) = body.get("password").and_then(Value::as_str).filter(|p| !p.is_empty()) else { + return err("bad_request"); + }; + if username.chars().count() > 512 || password.chars().count() > 4096 { + return err("bad_request"); + } + self.queue_save(origin, username.to_string(), Zeroizing::new(password.to_string())) + } + _ => err("unknown_type"), + } + } + + /// The item, if (and only if) it matches the requesting origin. The id alone is never enough. + fn matching_item(&self, origin: &str, id: &str) -> Result { + let candidates = self.core.items_for_url(origin.to_string()).map_err(|e| core_err(&e))?; + if !candidates.iter().any(|i| i.id == id) { + return Err(err("origin_mismatch")); + } + self.core.get_item(id.to_string()).map_err(|e| core_err(&e)) + } + + /// The existing login on this origin with this username, if the vault is unlocked. + fn existing(&self, origin: &str, username: &str) -> Option { + let items = self.core.items_for_url(origin.to_string()).ok()?; + let s = items.into_iter().find(|i| i.username.as_deref().unwrap_or("") == username)?; + self.core.get_item(s.id).ok() + } + + fn queue_save(&self, origin: String, username: String, password: Zeroizing) -> Value { + if let Some(e) = self.existing(&origin, &username) { + if e.password.as_deref() == Some(password.as_str()) { + return json!({ "type": "saved" }); // already up to date: no prompt + } + } + let save = PendingSave { id: uuid::Uuid::new_v4().to_string(), origin, username, password, at: Instant::now() }; + let view = self.save_view(&save); + { + let mut q = lock(&self.saves); + q.retain(|s| s.at.elapsed() < SAVE_TTL && !(s.origin == save.origin && s.username == save.username)); + if q.len() >= MAX_PENDING_SAVES { + q.remove(0); + } + q.push(save); + } + self.ui.save_requested(&view); + json!({ "type": "save_prompted" }) + } + + fn save_view(&self, s: &PendingSave) -> SaveView { + SaveView { + id: s.id.clone(), + origin: s.origin.clone(), + username: s.username.clone(), + update: self.existing(&s.origin, &s.username).is_some(), + } + } + + pub fn pending_saves(&self) -> Vec { + let mut q = lock(&self.saves); + q.retain(|s| s.at.elapsed() < SAVE_TTL); + let pending: Vec<_> = q.iter().map(|s| (s.id.clone(), s.origin.clone(), s.username.clone())).collect(); + drop(q); + pending + .into_iter() + .map(|(id, origin, username)| SaveView { + update: self.existing(&origin, &username).is_some(), + id, + origin, + username, + }) + .collect() + } + + /// The user answered a save prompt in the app. Returns the saved item id. + pub fn resolve_save(&self, id: &str, accept: bool) -> Result, String> { + let s = { + let mut q = lock(&self.saves); + let Some(pos) = q.iter().position(|s| s.id == id) else { return Err("This save request expired.".into()) }; + if accept && self.core.is_locked() { + return Err("Unlock Kestrove to save.".into()); + } + q.remove(pos) + }; + if !accept { + return Ok(None); + } + let item = match self.existing(&s.origin, &s.username) { + Some(mut e) => { + e.password = Some(s.password.to_string()); + e + } + None => ItemDto { + id: String::new(), + kind: ItemKindDto::Login, + title: url::Url::parse(&s.origin) + .ok() + .and_then(|u| u.host_str().map(|h| h.trim_start_matches("www.").to_string())) + .unwrap_or_else(|| s.origin.clone()), + username: (!s.username.is_empty()).then(|| s.username.clone()), + password: Some(s.password.to_string()), + urls: vec![s.origin.clone()], + totp: None, + notes: None, + favorite: false, + tags: vec![], + modified_at: 0, + }, + }; + self.core.save_item(item).map(Some).map_err(|e| e.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::channel::{make_confirm, Session as Sess}; + + struct AutoUi; + impl Ui for AutoUi { + fn pairing_requested(&self, _: &PairingView) {} + fn save_requested(&self, _: &SaveView) {} + } + + fn broker_with_item() -> (Arc, String) { + let core = VaultCore::new_in_memory(); + core.create_account("correct horse battery".into()).unwrap(); + let id = core + .save_item(ItemDto { + id: String::new(), + kind: ItemKindDto::Login, + title: "Example".into(), + username: Some("alice".into()), + password: Some("s3cret".into()), + urls: vec!["https://example.com".into()], + totp: Some("JBSWY3DPEHPK3PXP".into()), + notes: None, + favorite: false, + tags: vec![], + modified_at: 0, + }) + .unwrap(); + (Broker::new(core, Arc::new(AutoUi), PairingStore::in_memory()), id) + } + + /// Plays the extension: pairs, has the "user" approve in the app, opens a session. + fn pair_and_open(b: &Broker, conn: &mut Conn) -> Sess { + let ext = Ephemeral::generate(); + let r = b.handle_frame(conn, &json!({ "t": "pair_init", "pub": B64.encode(ext.public), "name": "Test" })); + assert_eq!(r["t"], "pair_started"); + let id = r["pairing_id"].as_str().unwrap().to_string(); + let app_pub: [u8; 32] = B64.decode(r["pub"].as_str().unwrap()).unwrap().try_into().unwrap(); + let shared_ext = ext.pair_as_ext(&app_pub).unwrap(); + assert_eq!(shared_ext.words.to_vec(), b.pending_pairing().unwrap().words); + let proof = B64.encode(make_confirm(&shared_ext.key, &id)); + let r = b.handle_frame(conn, &json!({ "t": "pair_confirm", "pairing_id": id, "proof": proof })); + assert_eq!(r["t"], "pair_waiting"); + b.resolve_pairing(&id, true).unwrap(); + let r = b.handle_frame(conn, &json!({ "t": "pair_confirm", "pairing_id": id, "proof": proof })); + assert_eq!(r["t"], "pair_done"); + + let ne = random::<16>(); + let r = b.handle_frame(conn, &json!({ "t": "open", "pairing_id": id, "nonce": B64.encode(ne) })); + assert_eq!(r["t"], "opened"); + let na: [u8; 16] = B64.decode(r["nonce"].as_str().unwrap()).unwrap().try_into().unwrap(); + Sess::ext(&shared_ext.key, &ne, &na) + } + + fn call(b: &Broker, conn: &mut Conn, s: &mut Sess, body: Value) -> Value { + let (n, c) = s.seal(&serde_json::to_vec(&json!({ "rid": 7, "body": body })).unwrap()); + let r = b.handle_frame(conn, &json!({ "t": "msg", "n": n, "c": B64.encode(c) })); + assert_eq!(r["t"], "msg", "{r}"); + let pt = s.open(r["n"].as_u64().unwrap(), &B64.decode(r["c"].as_str().unwrap()).unwrap()).unwrap(); + let v: Value = serde_json::from_slice(&pt).unwrap(); + assert_eq!(v["rid"], 7); + v["body"].clone() + } + + #[test] + fn pair_query_fill_and_origin_checks() { + let (b, id) = broker_with_item(); + let mut conn = Conn { caller: String::new(), session: None }; + let mut s = pair_and_open(&b, &mut conn); + + let r = call(&b, &mut conn, &mut s, json!({ "type": "query", "origin": "https://example.com" })); + assert_eq!(r["items"][0]["username"], "alice"); + assert!(r["items"][0].get("password").is_none()); + let r = call(&b, &mut conn, &mut s, json!({ "type": "fill", "origin": "https://example.com", "itemId": id })); + assert_eq!(r, json!({ "type": "credential", "username": "alice", "password": "s3cret" })); + let r = call(&b, &mut conn, &mut s, json!({ "type": "fill", "origin": "https://evil.test", "itemId": id })); + assert_eq!(r["code"], "origin_mismatch"); + let r = call(&b, &mut conn, &mut s, json!({ "type": "fill", "origin": "javascript:alert(1)", "itemId": id })); + assert_eq!(r["code"], "bad_request"); + let r = call(&b, &mut conn, &mut s, json!({ "type": "totp", "origin": "https://example.com", "itemId": id })); + assert_eq!(r["type"], "totp"); + assert_eq!(r["code"].as_str().unwrap().len(), 6); + + b.core().lock(); + let r = call(&b, &mut conn, &mut s, json!({ "type": "fill", "origin": "https://example.com", "itemId": id })); + assert_eq!(r["code"], "locked"); + } + + #[test] + fn replayed_frame_kills_the_session() { + let (b, _) = broker_with_item(); + let mut conn = Conn { caller: String::new(), session: None }; + let mut s = pair_and_open(&b, &mut conn); + let (n, c) = s.seal(br#"{"rid":1,"body":{"type":"hello","version":"1"}}"#); + let frame = json!({ "t": "msg", "n": n, "c": B64.encode(c) }); + assert_eq!(b.handle_frame(&mut conn, &frame)["t"], "msg"); + assert_eq!(b.handle_frame(&mut conn, &frame)["code"], "bad_session"); + assert_eq!(b.handle_frame(&mut conn, &frame)["code"], "bad_session"); + } + + #[test] + fn unpaired_and_revoked_extensions_are_refused() { + let (b, _) = broker_with_item(); + let mut conn = Conn { caller: String::new(), session: None }; + let r = b.handle_frame(&mut conn, &json!({ "t": "open", "pairing_id": "nope", "nonce": B64.encode([0u8; 16]) })); + assert_eq!(r["code"], "not_paired"); + let r = b.handle_frame(&mut conn, &json!({ "t": "msg", "n": 0, "c": "AAAA" })); + assert_eq!(r["code"], "bad_session"); + + let _ = pair_and_open(&b, &mut conn); + let id = b.pairings()[0].id.clone(); + b.revoke_pairing(&id).unwrap(); + let r = b.handle_frame(&mut conn, &json!({ "t": "open", "pairing_id": id, "nonce": B64.encode([0u8; 16]) })); + assert_eq!(r["code"], "not_paired"); + } + + #[test] + fn pairing_needs_unlock_and_rejects_bad_proof() { + let (b, _) = broker_with_item(); + let mut conn = Conn { caller: String::new(), session: None }; + let ext = Ephemeral::generate(); + let r = b.handle_frame(&mut conn, &json!({ "t": "pair_init", "pub": B64.encode(ext.public) })); + let id = r["pairing_id"].as_str().unwrap().to_string(); + let r = b.handle_frame(&mut conn, &json!({ "t": "pair_confirm", "pairing_id": id, "proof": B64.encode([0u8; 40]) })); + assert_eq!(r["code"], "not_paired"); + b.core().lock(); + assert!(b.resolve_pairing(&id, true).is_err()); + assert!(b.pending_pairing().is_some(), "still pending after a locked attempt"); + assert!(b.pairings().is_empty()); + } + + #[test] + fn save_prompts_then_creates_or_updates() { + let (b, id) = broker_with_item(); + let r = b.handle_request(&json!({ "type": "save", "origin": "https://example.com", "username": "alice", "password": "s3cret" })); + assert_eq!(r["type"], "saved"); // identical: nothing to ask + let r = b.handle_request(&json!({ "type": "save", "origin": "https://example.com/login", "username": "alice", "password": "new" })); + assert_eq!(r["type"], "save_prompted"); + let views = b.pending_saves(); + assert_eq!((views.len(), views[0].update, views[0].origin.as_str()), (1, true, "https://example.com")); + assert_eq!(b.resolve_save(&views[0].id, true).unwrap().as_deref(), Some(id.as_str())); + assert_eq!(b.core().get_item(id).unwrap().password.as_deref(), Some("new")); + + b.handle_request(&json!({ "type": "save", "origin": "https://new.example.org", "username": "bob", "password": "pw" })); + let v = b.pending_saves().pop().unwrap(); + assert!(!v.update); + let new_id = b.resolve_save(&v.id, true).unwrap().unwrap(); + let d = b.core().get_item(new_id).unwrap(); + assert_eq!((d.title.as_str(), d.urls[0].as_str()), ("new.example.org", "https://new.example.org")); + assert!(b.pending_saves().is_empty()); + } + + #[test] + fn normalizes_origins() { + assert_eq!(normalize_origin("https://Example.com:443/x?y").as_deref(), Some("https://example.com")); + assert_eq!(normalize_origin("http://localhost:8080").as_deref(), Some("http://localhost:8080")); + assert_eq!(normalize_origin("https://u:p@example.com"), None); + assert_eq!(normalize_origin("file:///etc/passwd"), None); + assert_eq!(normalize_origin("chrome-extension://abc/"), None); + } + + #[test] + fn pairing_store_persists_with_private_mode() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("pairings.json"); + let s = PairingStore::open(path.clone()).unwrap(); + s.add(StoredPairing { + id: "a".into(), + name: "n".into(), + caller: String::new(), + words: vec![], + created_at: 1, + key: B64.encode([1u8; 32]), + }) + .unwrap(); + assert_eq!(PairingStore::open(path.clone()).unwrap().list().len(), 1); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600); + } + assert!(s.remove("a").unwrap()); + assert!(PairingStore::open(path).unwrap().list().is_empty()); + } +} diff --git a/src-tauri/src/channel.rs b/src-tauri/src/channel.rs new file mode 100644 index 0000000..da56ad0 --- /dev/null +++ b/src-tauri/src/channel.rs @@ -0,0 +1,251 @@ +//! Pairing channel between the browser extension and the app (docs/05 "Pairing", threat A8). +//! +//! The same construction is implemented with WebCrypto in kestrove-browser/src/channel.ts; +//! the test vectors below are asserted on both sides. +//! +//! 1. **Pair.** Both sides generate an X25519 key. With `ss = X25519(ext, app)` and +//! `salt = SHA-256("kestrove/v1/pair" || ext_pub || app_pub)`: +//! `pairing_key = HKDF-SHA256(ss, salt, "kestrove/v1/pair/key", 32)` and +//! `fingerprint = HKDF-SHA256(ss, salt, "kestrove/v1/pair/fingerprint", 6)`, shown as 6 words +//! in both UIs. The user confirms they match in the app (which is what makes the pairing +//! trusted) and in the extension. A man in the middle would have to win a 2^-48 collision. +//! 2. **Session.** Per connection, each side sends a random 16-byte nonce and +//! `session_key = HKDF-SHA256(pairing_key, nonce_ext || nonce_app, "kestrove/v1/session", 32)`. +//! Fresh nonces on both sides mean a recorded session can't be replayed in a new one. +//! 3. **Messages.** AES-256-GCM, nonce `dir || 0x000000 || counter_be64`, AAD `"kestrove/v1/msg"`. +//! `dir` is 1 for extension → app and 2 for app → extension (no reflection). Each side expects +//! the exact next counter, so replays, drops and reordering within a session all fail. + +use aes_gcm::aead::{Aead, KeyInit, Payload}; +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use hkdf::Hkdf; +use sha2::{Digest, Sha256}; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +use crate::words::WORDS; + +pub const DIR_EXT_TO_APP: u8 = 1; +pub const DIR_APP_TO_EXT: u8 = 2; +const DIR_CONFIRM: u8 = 3; +const MSG_AAD: &[u8] = b"kestrove/v1/msg"; +const CONFIRM_AAD: &[u8] = b"kestrove/v1/pair/confirm"; + +#[derive(Debug, PartialEq, Eq)] +pub struct CryptoFailure; + +pub fn random() -> [u8; N] { + let mut b = [0u8; N]; + getrandom::getrandom(&mut b).expect("OS RNG unavailable"); + b +} + +/// The app's half of a pairing handshake. +pub struct Ephemeral { + secret: StaticSecret, + pub public: [u8; 32], +} + +impl Ephemeral { + pub fn generate() -> Self { + Self::from_secret(random()) + } + + pub fn from_secret(bytes: [u8; 32]) -> Self { + let secret = StaticSecret::from(bytes); + let public = PublicKey::from(&secret).to_bytes(); + Self { secret, public } + } + + /// The app's derivation of the pairing key and words. `None` for a low-order peer key. + pub fn pair(&self, ext_pub: &[u8; 32]) -> Option { + let ss = self.secret.diffie_hellman(&PublicKey::from(*ext_pub)); + ss.was_contributory().then(|| Pairing::derive(ss.as_bytes(), ext_pub, &self.public)) + } + + /// The extension's derivation (used by tests that play the extension). + pub fn pair_as_ext(&self, app_pub: &[u8; 32]) -> Option { + let ss = self.secret.diffie_hellman(&PublicKey::from(*app_pub)); + ss.was_contributory().then(|| Pairing::derive(ss.as_bytes(), &self.public, app_pub)) + } +} + +pub struct Pairing { + pub key: Zeroizing<[u8; 32]>, + pub words: [&'static str; 6], +} + +impl Pairing { + pub fn derive(shared: &[u8; 32], ext_pub: &[u8; 32], app_pub: &[u8; 32]) -> Self { + let salt = Sha256::new() + .chain_update(b"kestrove/v1/pair") + .chain_update(ext_pub) + .chain_update(app_pub) + .finalize(); + let hk = Hkdf::::new(Some(&salt), shared); + let mut key = Zeroizing::new([0u8; 32]); + hk.expand(b"kestrove/v1/pair/key", key.as_mut()).expect("32 bytes is a valid HKDF length"); + let mut fp = [0u8; 6]; + hk.expand(b"kestrove/v1/pair/fingerprint", &mut fp).expect("6 bytes is a valid HKDF length"); + Self { key, words: fp.map(|b| WORDS[b as usize]) } + } +} + +fn cipher(key: &[u8; 32]) -> Aes256Gcm { + Aes256Gcm::new(Key::::from_slice(key)) +} + +fn nonce(dir: u8, n: u64) -> [u8; 12] { + let mut out = [0u8; 12]; + out[0] = dir; + out[4..].copy_from_slice(&n.to_be_bytes()); + out +} + +/// Checks the extension's proof that it derived the same pairing key: AES-GCM of the pairing id. +pub fn verify_confirm(pairing_key: &[u8; 32], pairing_id: &str, proof: &[u8]) -> bool { + cipher(pairing_key) + .decrypt(Nonce::from_slice(&nonce(DIR_CONFIRM, 0)), Payload { msg: proof, aad: CONFIRM_AAD }) + .is_ok_and(|pt| pt == pairing_id.as_bytes()) +} + +/// The extension's proof (tests play the extension). +pub fn make_confirm(pairing_key: &[u8; 32], pairing_id: &str) -> Vec { + cipher(pairing_key) + .encrypt(Nonce::from_slice(&nonce(DIR_CONFIRM, 0)), Payload { msg: pairing_id.as_bytes(), aad: CONFIRM_AAD }) + .expect("AES-GCM encryption cannot fail for small inputs") +} + +/// One encrypted session over one connection. +pub struct Session { + cipher: Aes256Gcm, + send_dir: u8, + recv_dir: u8, + send_n: u64, + recv_n: u64, +} + +impl Session { + pub fn key(pairing_key: &[u8; 32], nonce_ext: &[u8; 16], nonce_app: &[u8; 16]) -> Zeroizing<[u8; 32]> { + let mut salt = [0u8; 32]; + salt[..16].copy_from_slice(nonce_ext); + salt[16..].copy_from_slice(nonce_app); + let mut key = Zeroizing::new([0u8; 32]); + Hkdf::::new(Some(&salt), pairing_key) + .expand(b"kestrove/v1/session", key.as_mut()) + .expect("32 bytes is a valid HKDF length"); + key + } + + /// The app's end: receives extension → app, sends app → extension. + pub fn app(pairing_key: &[u8; 32], nonce_ext: &[u8; 16], nonce_app: &[u8; 16]) -> Self { + Self::new(&Self::key(pairing_key, nonce_ext, nonce_app), DIR_APP_TO_EXT, DIR_EXT_TO_APP) + } + + /// The extension's end (tests stand in for the extension). + pub fn ext(pairing_key: &[u8; 32], nonce_ext: &[u8; 16], nonce_app: &[u8; 16]) -> Self { + Self::new(&Self::key(pairing_key, nonce_ext, nonce_app), DIR_EXT_TO_APP, DIR_APP_TO_EXT) + } + + fn new(key: &[u8; 32], send_dir: u8, recv_dir: u8) -> Self { + Self { cipher: cipher(key), send_dir, recv_dir, send_n: 0, recv_n: 0 } + } + + pub fn seal(&mut self, plaintext: &[u8]) -> (u64, Vec) { + let n = self.send_n; + self.send_n += 1; + let ct = self + .cipher + .encrypt(Nonce::from_slice(&nonce(self.send_dir, n)), Payload { msg: plaintext, aad: MSG_AAD }) + .expect("AES-GCM encryption cannot fail for small inputs"); + (n, ct) + } + + /// Opens the next message. Anything but the exact next counter is rejected (replay, reorder). + pub fn open(&mut self, n: u64, ciphertext: &[u8]) -> Result>, CryptoFailure> { + if n != self.recv_n { + return Err(CryptoFailure); + } + let pt = self + .cipher + .decrypt(Nonce::from_slice(&nonce(self.recv_dir, n)), Payload { msg: ciphertext, aad: MSG_AAD }) + .map_err(|_| CryptoFailure)?; + self.recv_n += 1; + Ok(Zeroizing::new(pt)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() + } + + /// Fixed vectors, also asserted in kestrove-browser/test/channel.test.ts. + #[test] + fn vectors() { + let ext = Ephemeral::from_secret([1u8; 32]); + let app = Ephemeral::from_secret([2u8; 32]); + assert_eq!(hex(&ext.public), "a4e09292b651c278b9772c569f5fa9bb13d906b46ab68c9df9dc2b4409f8a209"); + assert_eq!(hex(&app.public), "ce8d3ad1ccb633ec7b70c17814a5c76ecd029685050d344745ba05870e587d59"); + let p = app.pair(&ext.public).unwrap(); + let q = ext.pair_as_ext(&app.public).unwrap(); + assert_eq!((p.words, p.key.as_ref()), (q.words, q.key.as_ref())); + let fp = p.words.join(" "); + let key = hex(p.key.as_ref()); + assert_eq!(fp, VECTOR_WORDS); + assert_eq!(key, VECTOR_KEY); + + let mut s = Session::ext(&p.key, &[3u8; 16], &[4u8; 16]); + let (n, ct) = s.seal(br#"{"rid":0}"#); + assert_eq!(n, 0); + assert_eq!(hex(&ct), VECTOR_CT0); + assert_eq!(hex(&make_confirm(&p.key, "pid")), VECTOR_CONFIRM); + } + + const VECTOR_WORDS: &str = "cargo bucket blade cricket bamboo radar"; + const VECTOR_KEY: &str = "7dd958e1408eb2e772ed71c44763a05c7df77f6c7a6620d2e7e9049ec4573ae7"; + const VECTOR_CT0: &str = "f4204547c03bc11c2c0f1969ebcfd68985571ff7e10f7a6bd6"; + const VECTOR_CONFIRM: &str = "42eea959ea11c1e5aadcbcd3118c2f5878bab1"; + + #[test] + fn session_rejects_replay_reorder_and_reflection() { + let key = [9u8; 32]; + let (ne, na) = ([1u8; 16], [2u8; 16]); + let mut ext = Session::ext(&key, &ne, &na); + let mut app = Session::app(&key, &ne, &na); + + let (n0, c0) = ext.seal(b"one"); + let (n1, c1) = ext.seal(b"two"); + assert_eq!(app.open(n1, &c1), Err(CryptoFailure)); // out of order + assert_eq!(app.open(n0, &c0).unwrap().as_slice(), b"one"); + assert_eq!(app.open(n0, &c0), Err(CryptoFailure)); // replay + assert_eq!(app.open(n1, &c1).unwrap().as_slice(), b"two"); + + // An app → ext message can't be reflected back to the app. + let (rn, rc) = app.seal(b"reply"); + let mut app2 = Session::app(&key, &ne, &na); + assert_eq!(app2.open(rn, &rc), Err(CryptoFailure)); + assert_eq!(ext.open(rn, &rc).unwrap().as_slice(), b"reply"); + + // A different session (new app nonce) can't open old traffic. + let mut other = Session::app(&key, &ne, &[5u8; 16]); + assert_eq!(other.open(n0, &c0), Err(CryptoFailure)); + } + + #[test] + fn confirm_proof_binds_key_and_id() { + let key = [7u8; 32]; + let proof = make_confirm(&key, "abc"); + assert!(verify_confirm(&key, "abc", &proof)); + assert!(!verify_confirm(&key, "abd", &proof)); + assert!(!verify_confirm(&[8u8; 32], "abc", &proof)); + } + + #[test] + fn rejects_low_order_point() { + assert!(Ephemeral::generate().pair(&[0u8; 32]).is_none()); + } +} diff --git a/src-tauri/src/framing.rs b/src-tauri/src/framing.rs new file mode 100644 index 0000000..8919638 --- /dev/null +++ b/src-tauri/src/framing.rs @@ -0,0 +1,68 @@ +//! Length-prefixed JSON frames, the native-messaging wire format (a u32 length in native byte +//! order, then UTF-8 JSON). The same framing is used on the local IPC channel between +//! `kestrove-nmh` and the app, so the host can relay frames without parsing them. + +use std::io::{self, Read, Write}; + +use serde_json::Value; + +/// Chrome caps host → browser messages at 1 MiB; browser → host at 4 GiB, but we never need more than this. +pub const MAX_FRAME: usize = 1024 * 1024; + +/// Reads one raw frame. `Ok(None)` on a clean EOF before the length prefix. +pub fn read_frame(r: &mut impl Read) -> io::Result>> { + let mut len = [0u8; 4]; + match r.read_exact(&mut len) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e), + } + let len = u32::from_ne_bytes(len) as usize; + if len > MAX_FRAME { + return Err(io::Error::new(io::ErrorKind::InvalidData, "frame too large")); + } + let mut buf = vec![0u8; len]; + r.read_exact(&mut buf)?; + Ok(Some(buf)) +} + +pub fn write_frame(w: &mut impl Write, bytes: &[u8]) -> io::Result<()> { + if bytes.len() > MAX_FRAME { + return Err(io::Error::new(io::ErrorKind::InvalidData, "frame too large")); + } + w.write_all(&(bytes.len() as u32).to_ne_bytes())?; + w.write_all(bytes)?; + w.flush() +} + +pub fn read_json(r: &mut impl Read) -> io::Result> { + match read_frame(r)? { + Some(b) => serde_json::from_slice(&b).map(Some).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)), + None => Ok(None), + } +} + +pub fn write_json(w: &mut impl Write, v: &Value) -> io::Result<()> { + write_frame(w, &serde_json::to_vec(v)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn round_trip_and_eof() { + let mut buf = Vec::new(); + write_json(&mut buf, &json!({"t": "ping"})).unwrap(); + let mut r = buf.as_slice(); + assert_eq!(read_json(&mut r).unwrap(), Some(json!({"t": "ping"}))); + assert_eq!(read_json(&mut r).unwrap(), None); + } + + #[test] + fn rejects_oversized_length() { + let bytes = ((MAX_FRAME + 1) as u32).to_ne_bytes(); + assert!(read_frame(&mut bytes.as_slice()).is_err()); + } +} diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs new file mode 100644 index 0000000..4320f87 --- /dev/null +++ b/src-tauri/src/ipc.rs @@ -0,0 +1,450 @@ +//! Local IPC between `kestrove-nmh` (started by the browser) and the running app. +//! +//! * Unix: a socket in a 0700 per-user directory, socket mode 0600, and a peer-credential check +//! on both ends (`SO_PEERCRED` on Linux, `getpeereid` on macOS/BSD). Linux uses +//! `$XDG_RUNTIME_DIR/kestrove/nmh.sock`; macOS uses the per-user temp dir from +//! `confstr(_CS_DARWIN_USER_TEMP_DIR)`. +//! * Windows: the named pipe `\\.\pipe\kestrove-nmh-`, created first-instance-only (no +//! squatting), local clients only, with a DACL for the current user, plus a client token check. +//! +//! The channel carries only already-encrypted pairing frames (see `channel`), so this layer keeps +//! other users and remote machines out; the pairing key keeps other same-user processes out. +//! Traffic is strictly request → response: no concurrent read and write on one connection +//! (synchronous Windows pipe handles serialize I/O, so that would deadlock). + +use std::io::{self, Read, Write}; +use std::path::PathBuf; + +/// Overrides the IPC directory (Unix) or pipe name suffix (Windows). Tests use it to run an +/// isolated app; the browser passes its environment through to the host, so a browser started +/// with it set reaches that app. +pub const ENV_IPC_DIR: &str = "KESTROVE_IPC_DIR"; + +pub use imp::{connect, Conn, Listener}; + +/// Where the socket lives (Unix) or a display string for the pipe (Windows). +pub fn endpoint() -> io::Result { + imp::endpoint() +} + +impl Read for Conn { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } +} + +impl Write for Conn { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf) + } + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } +} + +#[cfg(unix)] +mod imp { + use std::fs; + use std::io; + use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt}; + use std::os::unix::io::AsRawFd; + use std::os::unix::net::{UnixListener, UnixStream}; + use std::path::PathBuf; + use std::time::Duration; + + use super::ENV_IPC_DIR; + + pub struct Conn(pub(super) UnixStream); + + pub struct Listener { + inner: UnixListener, + path: PathBuf, + } + + fn uid() -> u32 { + // SAFETY: geteuid has no preconditions and cannot fail. + unsafe { libc::geteuid() } + } + + fn base_dir() -> io::Result { + if let Some(d) = std::env::var_os(ENV_IPC_DIR).filter(|d| !d.is_empty()) { + return Ok(PathBuf::from(d)); + } + platform_dir() + } + + #[cfg(target_os = "macos")] + fn platform_dir() -> io::Result { + use std::ffi::{CStr, OsStr}; + use std::os::unix::ffi::OsStrExt; + let mut buf = vec![0u8; 1024]; + // SAFETY: buf is valid for buf.len() bytes; confstr NUL-terminates within that length. + let n = unsafe { libc::confstr(libc::_CS_DARWIN_USER_TEMP_DIR, buf.as_mut_ptr().cast(), buf.len()) }; + if n == 0 || n > buf.len() { + return Err(io::Error::last_os_error()); + } + let s = CStr::from_bytes_until_nul(&buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + Ok(PathBuf::from(OsStr::from_bytes(s.to_bytes())).join("kestrove")) + } + + #[cfg(not(target_os = "macos"))] + fn platform_dir() -> io::Result { + match std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from) { + Some(d) if d.is_absolute() => Ok(d.join("kestrove")), + // No runtime dir (e.g. no systemd session): a per-uid dir in /tmp, ownership checked in secure_dir. + _ => Ok(PathBuf::from(format!("/tmp/kestrove-{}", uid()))), + } + } + + pub fn endpoint() -> io::Result { + Ok(base_dir()?.join("nmh.sock")) + } + + /// Creates the directory 0700, or checks that an existing one is ours and not group/world accessible. + fn secure_dir(dir: &std::path::Path) -> io::Result<()> { + match fs::DirBuilder::new().recursive(true).mode(0o700).create(dir) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(e), + } + let meta = fs::symlink_metadata(dir)?; + if !meta.is_dir() || meta.uid() != uid() { + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC directory is not owned by this user")); + } + if meta.mode() & 0o077 != 0 { + fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?; + } + Ok(()) + } + + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(super) fn peer_uid(s: &UnixStream) -> io::Result { + let mut cred = libc::ucred { pid: 0, uid: 0, gid: 0 }; + let mut len = std::mem::size_of::() as libc::socklen_t; + // SAFETY: cred and len are valid out-pointers of the sizes passed. + let rc = unsafe { + libc::getsockopt( + s.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_PEERCRED, + (&mut cred as *mut libc::ucred).cast(), + &mut len, + ) + }; + if rc != 0 { + return Err(io::Error::last_os_error()); + } + Ok(cred.uid) + } + + #[cfg(not(any(target_os = "linux", target_os = "android")))] + pub(super) fn peer_uid(s: &UnixStream) -> io::Result { + let (mut uid, mut gid) = (0, 0); + // SAFETY: uid and gid are valid out-pointers. + if unsafe { libc::getpeereid(s.as_raw_fd(), &mut uid, &mut gid) } != 0 { + return Err(io::Error::last_os_error()); + } + Ok(uid) + } + + fn check_peer(s: &UnixStream) -> io::Result<()> { + if peer_uid(s)? != uid() { + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC peer is a different user")); + } + Ok(()) + } + + impl Listener { + /// Binds the socket. Fails with `AddrInUse` if another app instance is already serving it. + pub fn bind() -> io::Result { + let path = endpoint()?; + secure_dir(path.parent().expect("socket path has a parent"))?; + if path.exists() { + if UnixStream::connect(&path).is_ok() { + return Err(io::Error::new(io::ErrorKind::AddrInUse, "Kestrove is already running")); + } + fs::remove_file(&path)?; // stale socket from a crashed instance + } + let inner = UnixListener::bind(&path)?; + fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; + Ok(Self { inner, path }) + } + + /// Blocks for the next same-user client. Other users' connections are dropped and skipped. + pub fn accept(&self) -> io::Result { + loop { + let (s, _) = self.inner.accept()?; + if check_peer(&s).is_ok() { + // A stuck peer must not pin a thread forever. + s.set_read_timeout(Some(Duration::from_secs(600)))?; + return Ok(Conn(s)); + } + } + } + } + + impl Drop for Listener { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } + } + + pub fn connect() -> io::Result { + let s = UnixStream::connect(endpoint()?)?; + check_peer(&s)?; // don't talk to a socket another user planted + Ok(Conn(s)) + } +} + +#[cfg(windows)] +mod imp { + use std::fs::File; + use std::io; + use std::os::windows::ffi::OsStrExt; + use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; + use std::path::PathBuf; + use std::ptr; + + use windows_sys::Win32::Foundation::{ + CloseHandle, GetLastError, LocalFree, ERROR_PIPE_CONNECTED, GENERIC_READ, GENERIC_WRITE, HANDLE, + INVALID_HANDLE_VALUE, + }; + use windows_sys::Win32::Security::Authorization::{ + ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, + }; + use windows_sys::Win32::Security::{ + EqualSid, GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER, + }; + use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_FLAG_FIRST_PIPE_INSTANCE, OPEN_EXISTING, PIPE_ACCESS_DUPLEX, + }; + use windows_sys::Win32::System::Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, GetNamedPipeServerProcessId, + PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS, PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, + }; + use windows_sys::Win32::System::Threading::{ + GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION, + }; + + use super::ENV_IPC_DIR; + + pub struct Conn(pub(super) File); + + pub struct Listener { + name: Vec, + sddl: Vec, + first: std::sync::atomic::AtomicBool, + } + + fn wide(s: &str) -> Vec { + std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() + } + + fn pipe_name() -> String { + let suffix = std::env::var(ENV_IPC_DIR) + .ok() + .filter(|s| !s.is_empty()) + .or_else(|| std::env::var("USERNAME").ok()) + .unwrap_or_else(|| "user".into()); + let clean: String = suffix.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }).collect(); + format!(r"\\.\pipe\kestrove-nmh-{clean}") + } + + pub fn endpoint() -> io::Result { + Ok(PathBuf::from(pipe_name())) + } + + /// The TOKEN_USER buffer of a process (the SID pointer points into it). + fn token_user(process: HANDLE) -> io::Result> { + let mut token: HANDLE = ptr::null_mut(); + // SAFETY: valid process handle and out-pointer. + if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 { + return Err(io::Error::last_os_error()); + } + // SAFETY: we own the token handle from here on. + let token = unsafe { OwnedHandle::from_raw_handle(token) }; + let mut len = 0u32; + // SAFETY: a size query with a null buffer is the documented pattern. + unsafe { GetTokenInformation(token.as_raw_handle(), TokenUser, ptr::null_mut(), 0, &mut len) }; + let mut buf = vec![0u8; len as usize]; + // SAFETY: buf has len bytes. + if unsafe { GetTokenInformation(token.as_raw_handle(), TokenUser, buf.as_mut_ptr().cast(), len, &mut len) } == 0 + { + return Err(io::Error::last_os_error()); + } + Ok(buf) + } + + fn sid_of(buf: &[u8]) -> *mut core::ffi::c_void { + // SAFETY: buf came from GetTokenInformation(TokenUser) and starts with a TOKEN_USER. + unsafe { (*(buf.as_ptr() as *const TOKEN_USER)).User.Sid } + } + + fn same_user_as_process(pid: u32) -> io::Result<()> { + // SAFETY: plain Win32 calls; every handle is closed before returning. + unsafe { + let ours = token_user(GetCurrentProcess())?; + let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid); + if h.is_null() { + return Err(io::Error::last_os_error()); + } + let theirs = token_user(h); + CloseHandle(h); + let theirs = theirs?; + if EqualSid(sid_of(&ours), sid_of(&theirs)) == 0 { + return Err(io::Error::new(io::ErrorKind::PermissionDenied, "IPC peer is a different user")); + } + } + Ok(()) + } + + /// SDDL granting full access to the current user only (no Everyone read like the default DACL). + fn user_only_sddl() -> io::Result> { + // SAFETY: ConvertSidToStringSidW allocates with LocalAlloc; we free it after copying. + unsafe { + let ours = token_user(GetCurrentProcess())?; + let mut s: *mut u16 = ptr::null_mut(); + if ConvertSidToStringSidW(sid_of(&ours), &mut s) == 0 { + return Err(io::Error::last_os_error()); + } + let mut n = 0; + while *s.add(n) != 0 { + n += 1; + } + let sid = String::from_utf16_lossy(std::slice::from_raw_parts(s, n)); + LocalFree(s.cast()); + Ok(wide(&format!("D:P(A;;GA;;;{sid})"))) + } + } + + impl Listener { + pub fn bind() -> io::Result { + let me = Self { + name: wide(&pipe_name()), + sddl: user_only_sddl()?, + first: std::sync::atomic::AtomicBool::new(true), + }; + Ok(me) + } + + fn create_instance(&self) -> io::Result { + let first = self.first.swap(false, std::sync::atomic::Ordering::SeqCst); + let mut sd: PSECURITY_DESCRIPTOR = ptr::null_mut(); + // SAFETY: sddl is NUL-terminated; sd is freed with LocalFree below. + unsafe { + if ConvertStringSecurityDescriptorToSecurityDescriptorW( + self.sddl.as_ptr(), + SDDL_REVISION_1, + &mut sd, + ptr::null_mut(), + ) == 0 + { + return Err(io::Error::last_os_error()); + } + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: sd, + bInheritHandle: 0, + }; + let h = CreateNamedPipeW( + self.name.as_ptr(), + PIPE_ACCESS_DUPLEX | if first { FILE_FLAG_FIRST_PIPE_INSTANCE } else { 0 }, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, + PIPE_UNLIMITED_INSTANCES, + 64 * 1024, + 64 * 1024, + 0, + &sa, + ); + LocalFree(sd); + if h == INVALID_HANDLE_VALUE { + let e = io::Error::last_os_error(); + return Err(if first { io::Error::new(io::ErrorKind::AddrInUse, e) } else { e }); + } + Ok(h) + } + } + + pub fn accept(&self) -> io::Result { + loop { + let h = self.create_instance()?; + // SAFETY: h is a fresh pipe handle owned by `file` from here on. + let file = unsafe { File::from_raw_handle(h) }; + // SAFETY: blocking connect on our own pipe handle. + let ok = unsafe { ConnectNamedPipe(h, ptr::null_mut()) } != 0 + || unsafe { GetLastError() } == ERROR_PIPE_CONNECTED; + if !ok { + continue; + } + let mut pid = 0u32; + // SAFETY: valid handle and out-pointer. + if unsafe { GetNamedPipeClientProcessId(h, &mut pid) } != 0 && same_user_as_process(pid).is_ok() { + return Ok(Conn(file)); + } + } + } + } + + pub fn connect() -> io::Result { + let name = wide(&pipe_name()); + // SAFETY: name is NUL-terminated; the handle is owned by File on success. + let h = unsafe { + CreateFileW( + name.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + 0, + ptr::null(), + OPEN_EXISTING, + 0, + ptr::null_mut(), + ) + }; + if h == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + // SAFETY: h is a valid, owned handle. + let file = unsafe { File::from_raw_handle(h) }; + let mut pid = 0u32; + // SAFETY: valid handle and out-pointer. + if unsafe { GetNamedPipeServerProcessId(h, &mut pid) } == 0 { + return Err(io::Error::last_os_error()); + } + same_user_as_process(pid)?; + Ok(Conn(file)) + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use crate::framing::{read_json, write_json}; + use serde_json::json; + use std::os::unix::fs::PermissionsExt; + + #[test] + fn socket_is_private_and_round_trips() { + let dir = tempfile::tempdir().unwrap(); + let ipc_dir = dir.path().join("ipc"); + // SAFETY: tests in this module are the only ones touching this variable. + unsafe { std::env::set_var(ENV_IPC_DIR, &ipc_dir) }; + let l = Listener::bind().unwrap(); + let path = endpoint().unwrap(); + assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600); + assert_eq!(std::fs::metadata(&ipc_dir).unwrap().permissions().mode() & 0o777, 0o700); + + let t = std::thread::spawn(move || { + let mut c = l.accept().unwrap(); + let v = read_json(&mut c).unwrap().unwrap(); + write_json(&mut c, &json!({"echo": v})).unwrap(); + l + }); + let mut c = connect().unwrap(); + write_json(&mut c, &json!({"t": "ping"})).unwrap(); + assert_eq!(read_json(&mut c).unwrap(), Some(json!({"echo": {"t": "ping"}}))); + let l = t.join().unwrap(); + // A second instance sees the live socket and refuses to steal it. + assert_eq!(Listener::bind().err().map(|e| e.kind()), Some(io::ErrorKind::AddrInUse)); + drop(l); + assert!(!path.exists(), "socket removed on shutdown"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..a146395 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,9 @@ +//! Kestrove desktop internals shared by the app, `kestrove-nmh` and the integration tests. + +pub mod autolock; +pub mod broker; +pub mod channel; +pub mod framing; +pub mod ipc; +pub mod private_file; +pub mod words; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f931499..f2b1819 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,21 +1,59 @@ -// Kestrove desktop (Windows/Linux; macOS uses kestrove-apple). Thin Tauri shell over kv-ffi. +// Kestrove desktop (Windows/Linux; macOS uses kestrove-apple, but this builds and runs there for +// development). Thin Tauri shell over kv-ffi: the web UI only renders and never sees keys. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::{sync::Arc, thread, time::Duration}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{io, thread}; -use kv_ffi::{generate_password as core_generate_password, CoreError, ItemDto, ItemKindDto, VaultCore}; +use kestrove_desktop::autolock::{self, Activity, Reason}; +use kestrove_desktop::broker::{Broker, PairingStore, PairingView, SaveView, Ui}; +use kestrove_desktop::{ipc, private_file}; +use kv_ffi::{generate_password as core_generate_password, totp_code, CoreError, ItemDto, ItemKindDto, VaultCore}; use serde::{Deserialize, Serialize}; -use tauri::State; +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::TrayIconBuilder; +use tauri::{AppHandle, Emitter, Manager, State, WindowEvent}; +use zeroize::Zeroizing; + +const CLIPBOARD_CLEAR: Duration = Duration::from_secs(90); struct App { - // TODO(M3): persistent store + sync (kv-store/kv-sync) instead of in-memory. core: Arc, + broker: Arc, + activity: Arc, + data_dir: PathBuf, + ipc_error: Mutex>, + idle_minutes: Arc, + // Kept alive: on X11 the clipboard owner must outlive the copy. + clipboard: Mutex>, +} + +impl App { + fn secret_key_path(&self) -> PathBuf { + self.data_dir.join("secret-key") + } + fn settings_path(&self) -> PathBuf { + self.data_dir.join("settings.json") + } +} + +#[derive(Serialize, Deserialize)] +struct Settings { + idle_minutes: u64, } #[derive(Serialize)] struct AppState { has_account: bool, locked: bool, + /// The Secret Key is kept on this device, so unlock only needs the master password. + remembers_secret_key: bool, + /// Browser integration could not start (e.g. another instance is running). + ipc_error: Option, + idle_minutes: u64, } #[derive(Serialize)] @@ -25,6 +63,7 @@ struct Summary { username: Option, urls: Vec, favorite: bool, + has_totp: bool, } #[derive(Serialize)] @@ -44,6 +83,22 @@ struct NewLogin { username: Option, password: Option, urls: Vec, + totp: Option, +} + +#[derive(Serialize)] +struct Totp { + code: String, + seconds_remaining: u64, +} + +#[derive(Serialize)] +struct PairingSummary { + id: String, + name: String, + caller: String, + words: Vec, + created_at: u64, } type CmdResult = Result; @@ -52,20 +107,80 @@ fn err(e: CoreError) -> String { e.to_string() } +/// Tauri events to the UI; the window is brought forward so the user sees the prompt. +struct TauriUi { + handle: AppHandle, + activity: Arc, +} + +impl TauriUi { + fn surface(&self) { + if let Some(w) = self.handle.get_webview_window("main") { + let _ = w.show(); + let _ = w.unminimize(); + let _ = w.set_focus(); + } + } +} + +impl Ui for TauriUi { + fn pairing_requested(&self, req: &PairingView) { + self.surface(); + let _ = self.handle.emit("pairing-request", req); + } + fn save_requested(&self, req: &SaveView) { + self.surface(); + let _ = self.handle.emit("save-request", req); + } + fn activity(&self) { + self.activity.touch(); + } +} + +fn remember_secret_key(app: &App, secret_key: &str) { + if let Err(e) = private_file::write(&app.secret_key_path(), secret_key.trim().as_bytes()) { + eprintln!("kestrove: could not store the Secret Key: {e}"); + } +} + +fn stored_secret_key(app: &App) -> Option> { + let b = private_file::read(&app.secret_key_path()).ok()??; + String::from_utf8(b).ok().map(Zeroizing::new) +} + #[tauri::command] fn state(app: State<'_, App>) -> AppState { - AppState { has_account: app.core.has_account(), locked: app.core.is_locked() } + AppState { + has_account: app.core.has_account(), + locked: app.core.is_locked(), + remembers_secret_key: stored_secret_key(&app).is_some(), + ipc_error: app.ipc_error.lock().unwrap_or_else(|e| e.into_inner()).clone(), + idle_minutes: app.idle_minutes.load(Ordering::Relaxed), + } } // Argon2id takes ~0.5 s, so these run as async commands off the main thread. #[tauri::command] async fn create_account(app: State<'_, App>, master_password: String) -> CmdResult { - app.core.create_account(master_password).map_err(err) + let master_password = Zeroizing::new(master_password); + let sk = app.core.create_account(master_password.to_string()).map_err(err)?; + remember_secret_key(&app, &sk); + app.activity.touch(); + Ok(sk) } +/// `secret_key` may be omitted once this device remembers it. #[tauri::command] -async fn unlock(app: State<'_, App>, master_password: String, secret_key: String) -> CmdResult<()> { - app.core.unlock(master_password, secret_key).map_err(err) +async fn unlock(app: State<'_, App>, master_password: String, secret_key: Option) -> CmdResult<()> { + let master_password = Zeroizing::new(master_password); + let typed = secret_key.map(Zeroizing::new).filter(|s| !s.trim().is_empty()); + let sk = typed.clone().or_else(|| stored_secret_key(&app)).ok_or("Enter your Secret Key.")?; + app.core.unlock(master_password.to_string(), sk.trim().to_string()).map_err(err)?; + if let Some(sk) = typed { + remember_secret_key(&app, &sk); + } + app.activity.touch(); + Ok(()) } #[tauri::command] @@ -73,6 +188,20 @@ fn lock(app: State<'_, App>) { app.core.lock(); } +/// UI input (throttled in JS): feeds the idle timer where the OS has no system idle time. +#[tauri::command] +fn touch(app: State<'_, App>) { + app.activity.touch(); +} + +#[tauri::command] +fn set_idle_minutes(app: State<'_, App>, minutes: u64) -> CmdResult<()> { + let minutes = minutes.clamp(1, 240); + app.idle_minutes.store(minutes, Ordering::Relaxed); + let bytes = serde_json::to_vec(&Settings { idle_minutes: minutes }).map_err(|e| e.to_string())?; + private_file::write(&app.settings_path(), &bytes).map_err(|e| e.to_string()) +} + #[tauri::command] fn list_items(app: State<'_, App>) -> CmdResult> { Ok(app @@ -80,7 +209,14 @@ fn list_items(app: State<'_, App>) -> CmdResult> { .list_items() .map_err(err)? .into_iter() - .map(|s| Summary { id: s.id, title: s.title, username: s.username, urls: s.urls, favorite: s.favorite }) + .map(|s| Summary { + id: s.id, + title: s.title, + username: s.username, + urls: s.urls, + favorite: s.favorite, + has_totp: s.has_totp, + }) .collect()) } @@ -100,7 +236,7 @@ fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult { username: item.username, password: item.password, urls: item.urls, - totp: None, + totp: item.totp.filter(|t| !t.trim().is_empty()), notes: None, favorite: false, tags: vec![], @@ -109,15 +245,50 @@ fn save_item(app: State<'_, App>, item: NewLogin) -> CmdResult { .map_err(err) } -/// Copies the password from Rust so it never needs to round-trip through JS, then clears it after 90 s. -/// TODO(M3): on Windows also set ExcludeClipboardContentFromMonitorProcessing (docs/04). #[tauri::command] -fn copy_secret(app: State<'_, App>, id: String) -> CmdResult<()> { - let secret = app.core.get_item(id).map_err(err)?.password.unwrap_or_default(); - let mut cb = arboard::Clipboard::new().map_err(|e| e.to_string())?; - cb.set_text(secret.clone()).map_err(|e| e.to_string())?; +fn delete_item(app: State<'_, App>, id: String) -> CmdResult<()> { + app.core.delete_item(id).map_err(err) +} + +#[tauri::command] +fn totp(app: State<'_, App>, id: String) -> CmdResult> { + let Some(secret) = app.core.get_item(id).map_err(err)?.totp else { return Ok(None) }; + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + let c = totp_code(secret, now).map_err(err)?; + Ok(Some(Totp { code: c.code, seconds_remaining: c.seconds_remaining })) +} + +/// Copies from Rust so the secret never round-trips through JS, then clears it after 90 s. +#[tauri::command] +fn copy_secret(app: State<'_, App>, id: String, field: Option) -> CmdResult<()> { + let item = app.core.get_item(id).map_err(err)?; + let secret = Zeroizing::new(match field.as_deref() { + Some("username") => item.username.unwrap_or_default(), + Some("totp") => { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + item.totp.map(|t| totp_code(t, now)).transpose().map_err(err)?.map(|c| c.code).unwrap_or_default() + } + _ => item.password.unwrap_or_default(), + }); + let mut guard = app.clipboard.lock().unwrap_or_else(|e| e.into_inner()); + if guard.is_none() { + *guard = Some(arboard::Clipboard::new().map_err(|e| e.to_string())?); + } + let cb = guard.as_mut().expect("clipboard initialized above"); + #[cfg(windows)] + { + // docs/04: keep secrets out of clipboard history, cloud sync and monitors. + use arboard::SetExtWindows; + cb.set().exclude_from_monitoring().exclude_from_cloud().exclude_from_history().text(secret.to_string()) + } + #[cfg(not(windows))] + { + cb.set_text(secret.to_string()) + } + .map_err(|e| e.to_string())?; + drop(guard); thread::spawn(move || { - thread::sleep(Duration::from_secs(90)); + thread::sleep(CLIPBOARD_CLEAR); if let Ok(mut cb) = arboard::Clipboard::new() { if cb.get_text().ok().as_deref() == Some(secret.as_str()) { let _ = cb.clear(); @@ -129,22 +300,187 @@ fn copy_secret(app: State<'_, App>, id: String) -> CmdResult<()> { #[tauri::command] fn generate_password(length: u32) -> String { - core_generate_password(length, true) + core_generate_password(length.clamp(8, 128), true) +} + +// ---------------------------------------------------------------- browser pairing + saves + +#[tauri::command] +fn pending_pairing(app: State<'_, App>) -> Option { + app.broker.pending_pairing() +} + +#[tauri::command] +fn resolve_pairing(app: State<'_, App>, id: String, accept: bool) -> CmdResult<()> { + app.broker.resolve_pairing(&id, accept) +} + +#[tauri::command] +fn list_pairings(app: State<'_, App>) -> Vec { + app.broker + .pairings() + .into_iter() + .map(|p| PairingSummary { id: p.id, name: p.name, caller: p.caller, words: p.words, created_at: p.created_at }) + .collect() +} + +#[tauri::command] +fn revoke_pairing(app: State<'_, App>, id: String) -> CmdResult<()> { + app.broker.revoke_pairing(&id) +} + +#[tauri::command] +fn pending_saves(app: State<'_, App>) -> Vec { + app.broker.pending_saves() +} + +#[tauri::command] +fn resolve_save(app: State<'_, App>, id: String, accept: bool) -> CmdResult<()> { + app.broker.resolve_save(&id, accept).map(|_| ()) +} + +// ---------------------------------------------------------------- startup + +fn start_ipc(broker: Arc) -> io::Result<()> { + let listener = ipc::Listener::bind()?; + thread::Builder::new().name("ipc-accept".into()).spawn(move || loop { + match listener.accept() { + Ok(conn) => { + let b = broker.clone(); + let _ = thread::Builder::new().name("ipc-conn".into()).spawn(move || { + let _ = b.serve(conn); + }); + } + Err(e) => { + eprintln!("kestrove: IPC accept failed: {e}"); + thread::sleep(Duration::from_secs(1)); + } + } + })?; + Ok(()) +} + +fn lock_and_notify(handle: &AppHandle, core: &VaultCore, reason: &str) { + core.lock(); + let _ = handle.emit("locked", reason); +} + +fn setup(app: &mut tauri::App) -> Result<(), Box> { + let data_dir = app.path().app_data_dir()?; + std::fs::create_dir_all(&data_dir)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&data_dir, std::fs::Permissions::from_mode(0o700))?; + } + let core = VaultCore::open(data_dir.join("vault.sqlite").to_string_lossy().into_owned())?; + let activity = Activity::new(); + let ui = Arc::new(TauriUi { handle: app.handle().clone(), activity: activity.clone() }); + let broker = Broker::new(core.clone(), ui, PairingStore::open(data_dir.join("pairings.json"))?); + + let idle_minutes = private_file::read(&data_dir.join("settings.json")) + .ok() + .flatten() + .and_then(|b| serde_json::from_slice::(&b).ok()) + .map_or(autolock::DEFAULT_IDLE.as_secs() / 60, |s| s.idle_minutes); + let idle_minutes = Arc::new(AtomicU64::new(idle_minutes)); + + let ipc_error = start_ipc(broker.clone()).err().map(|e| { + eprintln!("kestrove: browser integration disabled: {e}"); + if e.kind() == io::ErrorKind::AddrInUse { + "Another Kestrove instance is serving the browser extension.".to_string() + } else { + format!("Browser integration could not start: {e}") + } + }); + + { + let (handle, core, minutes) = (app.handle().clone(), core.clone(), idle_minutes.clone()); + let unlocked = core.clone(); + autolock::spawn( + activity.clone(), + move || Duration::from_secs(minutes.load(Ordering::Relaxed) * 60), + move || !unlocked.is_locked(), + move |reason| { + let why = match reason { + Reason::Idle => "idle", + Reason::ScreenLocked => "screen-locked", + Reason::Sleep => "sleep", + }; + lock_and_notify(&handle, &core, why); + }, + ); + } + + // Tray: the app keeps running (for the browser extension) when its window is closed. + let open = MenuItem::with_id(app, "open", "Open Kestrove", true, None::<&str>)?; + let lock_item = MenuItem::with_id(app, "lock", "Lock", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&open, &lock_item, &quit])?; + let mut tray = TrayIconBuilder::with_id("main").menu(&menu).tooltip("Kestrove").on_menu_event({ + let core = core.clone(); + move |handle, event| match event.id().as_ref() { + "open" => { + if let Some(w) = handle.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + "lock" => lock_and_notify(handle, &core, "manual"), + "quit" => { + core.lock(); + handle.exit(0); + } + _ => {} + } + }); + if let Some(icon) = app.default_window_icon() { + tray = tray.icon(icon.clone()); + } + tray.build(app)?; + + app.manage(App { + core, + broker, + activity, + data_dir, + ipc_error: Mutex::new(ipc_error), + idle_minutes, + clipboard: Mutex::new(None), + }); + Ok(()) } fn main() { tauri::Builder::default() - .manage(App { core: VaultCore::new_in_memory() }) + .setup(setup) + .on_window_event(|window, event| { + if let WindowEvent::CloseRequested { api, .. } = event { + // Keep serving the extension from the tray; Quit is in the tray menu. + api.prevent_close(); + let _ = window.hide(); + } + }) .invoke_handler(tauri::generate_handler![ state, create_account, unlock, lock, + touch, + set_idle_minutes, list_items, get_item, save_item, + delete_item, + totp, copy_secret, - generate_password + generate_password, + pending_pairing, + resolve_pairing, + list_pairings, + revoke_pairing, + pending_saves, + resolve_save ]) .run(tauri::generate_context!()) .expect("error while running Kestrove"); diff --git a/src-tauri/src/private_file.rs b/src-tauri/src/private_file.rs new file mode 100644 index 0000000..fd3703a --- /dev/null +++ b/src-tauri/src/private_file.rs @@ -0,0 +1,27 @@ +//! Small files only this user may read (0600 on Unix; on Windows the per-user app-data ACL). + +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +/// Writes atomically (temp file + rename) with owner-only permissions. +pub fn write(path: &Path, bytes: &[u8]) -> io::Result<()> { + let tmp = path.with_extension("tmp"); + let mut opts = fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::mode(&mut opts, 0o600); + let mut f = opts.open(&tmp)?; + f.write_all(bytes)?; + f.sync_all()?; + fs::rename(tmp, path) +} + +/// `Ok(None)` when the file doesn't exist. +pub fn read(path: &Path) -> io::Result>> { + match fs::read(path) { + Ok(b) => Ok(Some(b)), + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e), + } +} diff --git a/src-tauri/src/words.rs b/src-tauri/src/words.rs new file mode 100644 index 0000000..7f9b897 --- /dev/null +++ b/src-tauri/src/words.rs @@ -0,0 +1,48 @@ +//! 256 words for pairing fingerprints: one word per byte, 6 words = 48 bits, compared by a person. +//! The same list, in the same order, is in kestrove-browser/src/words.ts. + +pub const WORDS: [&str; 256] = [ + "acid", "acorn", "actor", "adult", "agent", "alarm", "album", "alley", + "amber", "angle", "ankle", "apple", "april", "apron", "arena", "armor", + "arrow", "atlas", "attic", "audio", "aunt", "autumn", "award", "bacon", + "badge", "bagel", "baker", "bamboo", "banjo", "barn", "basil", "basket", + "beach", "beard", "beetle", "bell", "bench", "berry", "bike", "birch", + "bison", "blade", "blanket", "blimp", "bloom", "boat", "bonus", "boot", + "bottle", "boxer", "brain", "brick", "bridge", "broom", "brush", "bucket", + "bugle", "cabin", "cable", "cactus", "camel", "camera", "canal", "candle", + "canoe", "canyon", "carbon", "cargo", "carpet", "castle", "cedar", "cello", + "chalk", "cherry", "chess", "chief", "cider", "cinema", "circus", "clay", + "cliff", "clock", "cloud", "clover", "cobra", "cocoa", "comet", "coral", + "cotton", "cousin", "coyote", "crane", "crayon", "cricket", "crown", "cup", + "daisy", "dancer", "delta", "denim", "desert", "diary", "diesel", "dingo", + "disco", "doctor", "dolphin", "donkey", "dragon", "drum", "duck", "eagle", + "earth", "easel", "echo", "eclipse", "elbow", "elder", "ember", "emerald", + "engine", "falcon", "fang", "farmer", "feather", "fence", "ferry", "fiddle", + "finch", "flag", "flute", "forest", "fossil", "fox", "frost", "galaxy", + "garden", "garlic", "gecko", "ghost", "ginger", "giraffe", "glacier", "globe", + "goat", "gold", "gorilla", "grape", "gravel", "guitar", "hammer", "harbor", + "harp", "hawk", "hazel", "helmet", "heron", "hiker", "honey", "hornet", + "hotel", "igloo", "index", "iris", "island", "ivory", "jacket", "jaguar", + "jelly", "jewel", "jungle", "kayak", "kettle", "kitten", "koala", "ladder", + "lagoon", "lake", "lantern", "laser", "lemon", "lentil", "lily", "lion", + "lizard", "lobster", "locket", "lotus", "magnet", "mango", "maple", "marble", + "meadow", "melon", "mint", "mirror", "monkey", "moose", "mosaic", "motor", + "mountain", "mulberry", "museum", "napkin", "nectar", "needle", "nickel", "noodle", + "novel", "nugget", "oasis", "ocean", "olive", "onion", "orange", "orbit", + "orchid", "otter", "owl", "oyster", "paddle", "palace", "panda", "paper", + "parrot", "peach", "pebble", "pepper", "piano", "pickle", "pilot", "pine", + "planet", "plum", "poem", "pony", "puzzle", "quartz", "quill", "rabbit", + "radar", "radio", "raven", "ribbon", "river", "robin", "rocket", "rose", + "ruby", "saddle", "salmon", "sandal", "satin", "scarf", "scout", "sugar", +]; + +#[cfg(test)] +mod tests { + #[test] + fn unique() { + let mut v = super::WORDS.to_vec(); + v.sort_unstable(); + v.dedup(); + assert_eq!(v.len(), 256); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 962beeb..8e58b2b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,6 +18,6 @@ "bundle": { "active": true, "targets": ["msi", "nsis", "appimage", "deb"], - "icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico", "icons/icon.png"] + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico", "icons/icon.png"] } } diff --git a/src-tauri/tests/nmh_stdio.rs b/src-tauri/tests/nmh_stdio.rs new file mode 100644 index 0000000..86e0966 --- /dev/null +++ b/src-tauri/tests/nmh_stdio.rs @@ -0,0 +1,148 @@ +//! Drives the real `kestrove-nmh` binary over stdio, the way a browser does, through the +//! paired AEAD channel to a broker serving the app's IPC endpoint (isolated via KESTROVE_IPC_DIR). +#![cfg(unix)] + +use std::io::{BufReader, Write}; +use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; +use std::sync::Arc; +use std::thread; + +use base64::engine::general_purpose::STANDARD as B64; +use base64::Engine; +use kestrove_desktop::broker::{Broker, PairingStore, PairingView, SaveView, Ui}; +use kestrove_desktop::channel::{make_confirm, random, Ephemeral, Session}; +use kestrove_desktop::framing::{read_json, write_json}; +use kestrove_desktop::ipc; +use kv_ffi::{ItemDto, ItemKindDto, VaultCore}; +use serde_json::{json, Value}; + +struct NoUi; +impl Ui for NoUi { + fn pairing_requested(&self, _: &PairingView) {} + fn save_requested(&self, _: &SaveView) {} +} + +struct Host { + child: Child, + stdin: ChildStdin, + stdout: BufReader, +} + +impl Host { + fn spawn(ipc_dir: &std::path::Path) -> Self { + let mut child = Command::new(env!("CARGO_BIN_EXE_kestrove-nmh")) + .arg("chrome-extension://abcdefghijklmnopabcdefghijklmnop/") + .env(ipc::ENV_IPC_DIR, ipc_dir) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let stdin = child.stdin.take().unwrap(); + let stdout = BufReader::new(child.stdout.take().unwrap()); + Self { child, stdin, stdout } + } + + fn rpc(&mut self, v: Value) -> Value { + write_json(&mut self.stdin, &v).unwrap(); + self.stdin.flush().unwrap(); + read_json(&mut self.stdout).unwrap().unwrap() + } +} + +impl Drop for Host { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn call(h: &mut Host, s: &mut Session, rid: u64, body: Value) -> Value { + let (n, c) = s.seal(&serde_json::to_vec(&json!({ "rid": rid, "body": body })).unwrap()); + let r = h.rpc(json!({ "t": "msg", "n": n, "c": B64.encode(c) })); + assert_eq!(r["t"], "msg", "{r}"); + let pt = s.open(r["n"].as_u64().unwrap(), &B64.decode(r["c"].as_str().unwrap()).unwrap()).unwrap(); + let v: Value = serde_json::from_slice(&pt).unwrap(); + assert_eq!(v["rid"], rid); + v["body"].clone() +} + +#[test] +fn pair_and_fill_through_the_native_host() { + let dir = tempfile::tempdir().unwrap(); + let ipc_dir = dir.path().join("ipc"); + + // No app yet: the host answers "unavailable" instead of hanging. + let mut early = Host::spawn(&ipc_dir); + assert_eq!(early.rpc(json!({ "t": "ping" })), json!({ "t": "error", "code": "unavailable" })); + + let core = VaultCore::new_in_memory(); + core.create_account("correct horse battery".into()).unwrap(); + let id = core + .save_item(ItemDto { + id: String::new(), + kind: ItemKindDto::Login, + title: "Local".into(), + username: Some("alice".into()), + password: Some("pw-123".into()), + urls: vec!["http://127.0.0.1:8765".into()], + totp: None, + notes: None, + favorite: false, + tags: vec![], + modified_at: 0, + }) + .unwrap(); + let broker = Broker::new(core, Arc::new(NoUi), PairingStore::in_memory()); + // SAFETY: this test binary has a single test, so nothing else reads the environment concurrently. + unsafe { std::env::set_var(ipc::ENV_IPC_DIR, &ipc_dir) }; + let listener = ipc::Listener::bind().unwrap(); + { + let b = broker.clone(); + thread::spawn(move || loop { + let conn = listener.accept().unwrap(); + let b = b.clone(); + thread::spawn(move || b.serve(conn)); + }); + } + + // The same host process reconnects once the app is up. + assert_eq!(early.rpc(json!({ "t": "ping" }))["t"], "pong"); + drop(early); + + let mut h = Host::spawn(&ipc_dir); + let ext = Ephemeral::generate(); + let r = h.rpc(json!({ "t": "pair_init", "pub": B64.encode(ext.public), "name": "Test browser" })); + assert_eq!(r["t"], "pair_started"); + let pid = r["pairing_id"].as_str().unwrap().to_string(); + let p = ext.pair_as_ext(&B64.decode(r["pub"].as_str().unwrap()).unwrap().try_into().unwrap()).unwrap(); + let pending = broker.pending_pairing().unwrap(); + assert_eq!(pending.words, p.words.to_vec(), "both sides show the same words"); + assert_eq!(pending.caller, "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"); + let proof = B64.encode(make_confirm(&p.key, &pid)); + assert_eq!(h.rpc(json!({ "t": "pair_confirm", "pairing_id": pid, "proof": proof }))["t"], "pair_waiting"); + broker.resolve_pairing(&pid, true).unwrap(); // the user clicks "pair" in the app + assert_eq!(h.rpc(json!({ "t": "pair_confirm", "pairing_id": pid, "proof": proof }))["t"], "pair_done"); + + let ne = random::<16>(); + let r = h.rpc(json!({ "t": "open", "pairing_id": pid, "nonce": B64.encode(ne) })); + let na: [u8; 16] = B64.decode(r["nonce"].as_str().unwrap()).unwrap().try_into().unwrap(); + let mut s = Session::ext(&p.key, &ne, &na); + + let ids = call(&mut h, &mut s, 1, json!({ "type": "query", "origin": "http://127.0.0.1:8765" })); + assert_eq!(ids["items"][0]["id"], id); + let cred = call(&mut h, &mut s, 2, json!({ "type": "fill", "origin": "http://127.0.0.1:8765", "itemId": id })); + assert_eq!(cred, json!({ "type": "credential", "username": "alice", "password": "pw-123" })); + let other = call(&mut h, &mut s, 3, json!({ "type": "fill", "origin": "http://127.0.0.2:8765", "itemId": id })); + assert_eq!(other["code"], "origin_mismatch"); + + // A replayed ciphertext is refused and the session is dropped. + let (n, c) = s.seal(br#"{"rid":4,"body":{"type":"hello","version":"1"}}"#); + let frame = json!({ "t": "msg", "n": n, "c": B64.encode(c) }); + assert_eq!(h.rpc(frame.clone())["t"], "msg"); + assert_eq!(h.rpc(frame)["code"], "bad_session"); + + // A second host process (new browser connection) needs a fresh session; the old one is useless there. + let mut h2 = Host::spawn(&ipc_dir); + let (n, c) = Session::ext(&p.key, &ne, &na).seal(b"{}"); + assert_eq!(h2.rpc(json!({ "t": "msg", "n": n, "c": B64.encode(c) }))["code"], "bad_session"); +} diff --git a/src/main.ts b/src/main.ts index 3e83afd..4b259f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,64 +1,90 @@ -// Minimal desktop UI. All vault logic runs in Rust (src-tauri → kv-ffi); this layer only renders. +// Desktop UI. All vault logic runs in Rust (src-tauri → kv-ffi); this layer only renders. import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; -interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean } +interface AppState { has_account: boolean; locked: boolean; remembers_secret_key: boolean; ipc_error: string | null; idle_minutes: number } +interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean; has_totp: boolean } interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null } +interface PairingView { id: string; name: string; caller: string; words: string[] } +interface Pairing { id: string; name: string; caller: string; words: string[]; created_at: number } +interface SaveView { id: string; origin: string; username: string; update: boolean } const app = document.getElementById("app")!; +const overlay = document.getElementById("overlay")!; -function el(tag: K, props: Record = {}, ...children: (Node | string)[]): HTMLElementTagNameMap[K] { +function el(tag: K, props: Partial & Record = {}, ...children: (Node | string)[]): HTMLElementTagNameMap[K] { const e: HTMLElementTagNameMap[K] = Object.assign(document.createElement(tag), props); e.append(...children); return e; } +const errText = (e: unknown) => (typeof e === "string" ? e : e instanceof Error ? e.message : String(e)); + async function render() { - const state = await invoke<{ has_account: boolean; locked: boolean }>("state"); + const state = await invoke("state"); app.replaceChildren(); - if (!state.has_account) return renderCreate(); - if (state.locked) return renderUnlock(); - return renderVault(); + if (!state.has_account) renderCreate(); + else if (state.locked) renderUnlock(state); + else await renderVault(state); + await renderPrompts(state); } +// ------------------------------------------------------------------ account + function renderCreate() { const pw = el("input", { type: "password", placeholder: "Master password (10+ chars)" }); const err = el("div", { className: "err" }); - const btn = el("button", { textContent: "Create vault" }); + const btn = el("button", { textContent: "Create vault", className: "primary" }); const card = el("div", { className: "card" }, el("h2", { textContent: "Create your Kestrove vault" }), pw, btn, err); btn.onclick = async () => { + btn.disabled = true; try { const secretKey = await invoke("create_account", { masterPassword: pw.value }); + pw.value = ""; card.replaceChildren( el("h2", { textContent: "Your Secret Key" }), el("p", { className: "mono", textContent: secretKey }), - el("p", { className: "muted", textContent: "Write this down in your Emergency Kit. You need it to add devices." }), - el("button", { textContent: "Continue", onclick: render }), + el("p", { className: "muted", textContent: "Write this down in your Emergency Kit. You need it to add devices. This device remembers it." }), + el("button", { textContent: "Continue", className: "primary", onclick: render }), ); } catch (e) { - err.textContent = String(e); + err.textContent = errText(e); + btn.disabled = false; } }; app.append(el("div", { className: "center" }, card)); + pw.focus(); } -function renderUnlock() { +function renderUnlock(state: AppState) { const pw = el("input", { type: "password", placeholder: "Master password" }); const sk = el("input", { placeholder: "Secret Key (K1-…)", className: "mono" }); const err = el("div", { className: "err" }); - const btn = el("button", { textContent: "Unlock" }); - btn.onclick = async () => { + const btn = el("button", { textContent: "Unlock", className: "primary" }); + const submit = async () => { + btn.disabled = true; try { - await invoke("unlock", { masterPassword: pw.value, secretKey: sk.value }); + await invoke("unlock", { masterPassword: pw.value, secretKey: sk.value || null }); pw.value = ""; + sk.value = ""; await render(); } catch (e) { - err.textContent = String(e); + err.textContent = errText(e); + btn.disabled = false; } }; - app.append(el("div", { className: "center" }, el("div", { className: "card" }, el("h2", { textContent: "Kestrove is locked" }), pw, sk, btn, err))); + btn.onclick = submit; + pw.onkeydown = (e) => e.key === "Enter" && void submit(); + const card = el("div", { className: "card" }, el("h2", { textContent: "Kestrove is locked" }), pw); + if (!state.remembers_secret_key) card.append(sk); + card.append(btn, err); + app.append(el("div", { className: "center" }, card)); + pw.focus(); } -async function renderVault() { +// ------------------------------------------------------------------ vault + +async function renderVault(state: AppState) { const items = await invoke("list_items"); const detail = el("section"); const list = el("ul"); @@ -67,24 +93,55 @@ async function renderVault() { li.onclick = async () => { list.querySelectorAll("li").forEach((n) => n.classList.remove("sel")); li.classList.add("sel"); - const item = await invoke("get_item", { id: it.id }); - const pwSpan = el("span", { className: "mono", textContent: "••••••••" }); - detail.replaceChildren( - el("h2", { textContent: item.title }), - el("p", {}, "Username: ", el("span", { className: "mono", textContent: item.username ?? "" })), - el("p", {}, "Password: ", pwSpan, " ", - el("button", { textContent: "Reveal", onclick: () => (pwSpan.textContent = item.password ?? "") }), - " ", - el("button", { textContent: "Copy", onclick: () => invoke("copy_secret", { id: item.id }) })), - el("p", { className: "muted", textContent: item.urls.join(", ") }), - ); + await renderDetail(detail, it.id); }; list.append(li); } - const add = el("button", { textContent: "+ New login" }); - add.onclick = () => renderEditor(detail); + if (!items.length) list.append(el("li", { className: "muted", textContent: "No items yet." })); + const add = el("button", { textContent: "+ New login", onclick: () => renderEditor(detail) }); + const settings = el("button", { textContent: "Settings", onclick: () => renderSettings(detail, state) }); const lock = el("button", { textContent: "Lock", onclick: async () => { await invoke("lock"); await render(); } }); - app.append(el("aside", {}, el("div", { className: "toolbar" }, add, lock), list), detail); + const aside = el("aside", {}, el("div", { className: "toolbar" }, add, settings, lock), list); + if (state.ipc_error) aside.append(el("p", { className: "err pad", textContent: state.ipc_error })); + app.append(aside, detail); +} + +async function renderDetail(target: HTMLElement, id: string) { + const item = await invoke("get_item", { id }); + const pwSpan = el("span", { className: "mono", textContent: "••••••••" }); + const rows: Node[] = [ + el("h2", { textContent: item.title }), + el("p", {}, "Username: ", el("span", { className: "mono", textContent: item.username ?? "" }), " ", + el("button", { textContent: "Copy", onclick: () => invoke("copy_secret", { id, field: "username" }) })), + el("p", {}, "Password: ", pwSpan, " ", + el("button", { textContent: "Reveal", onclick: () => (pwSpan.textContent = item.password ?? "") }), " ", + el("button", { textContent: "Copy", onclick: () => invoke("copy_secret", { id, field: "password" }) })), + ]; + if (item.totp) { + const code = el("span", { className: "mono" }); + const tick = async () => { + if (!code.isConnected) return; + const t = await invoke<{ code: string; seconds_remaining: number } | null>("totp", { id }).catch(() => null); + code.textContent = t ? `${t.code} (${t.seconds_remaining}s)` : "—"; + setTimeout(tick, 1000); + }; + rows.push(el("p", {}, "One-time code: ", code, " ", el("button", { textContent: "Copy", onclick: () => invoke("copy_secret", { id, field: "totp" }) }))); + queueMicrotask(tick); + } + rows.push( + el("p", { className: "muted", textContent: item.urls.join(", ") }), + el("button", { + textContent: "Delete", + className: "danger", + onclick: async () => { + if (confirm(`Delete “${item.title}”?`)) { + await invoke("delete_item", { id }); + await render(); + } + }, + }), + ); + target.replaceChildren(...rows); } function renderEditor(target: HTMLElement) { @@ -92,15 +149,129 @@ function renderEditor(target: HTMLElement) { const user = el("input", { placeholder: "Username" }); const pass = el("input", { placeholder: "Password", className: "mono" }); const url = el("input", { placeholder: "https://example.com" }); + const totp = el("input", { placeholder: "TOTP secret or otpauth:// URI (optional)", className: "mono" }); const gen = el("button", { textContent: "Generate", onclick: async () => (pass.value = await invoke("generate_password", { length: 24 })) }); - const save = el("button", { textContent: "Save" }); + const err = el("div", { className: "err" }); + const save = el("button", { textContent: "Save", className: "primary" }); save.onclick = async () => { - await invoke("save_item", { - item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [] }, - }); - await render(); + try { + await invoke("save_item", { + item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [], totp: totp.value || null }, + }); + await render(); + } catch (e) { + err.textContent = errText(e); + } }; - target.replaceChildren(el("div", { className: "card" }, el("h2", { textContent: "New login" }), title, user, pass, gen, url, save)); + target.replaceChildren(el("div", { className: "card" }, el("h2", { textContent: "New login" }), title, user, el("div", { className: "row" }, pass, gen), url, totp, save, err)); +} + +async function renderSettings(target: HTMLElement, state: AppState) { + const pairings = await invoke("list_pairings"); + const idle = el("select", {}, ...[1, 5, 10, 15, 30, 60, 120].map((m) => el("option", { value: String(m), textContent: `${m} min`, selected: m === state.idle_minutes }))); + idle.onchange = () => invoke("set_idle_minutes", { minutes: Number(idle.value) }); + const list = el("ul", { className: "plain" }); + for (const p of pairings) { + list.append( + el("li", {}, + el("div", { textContent: `${p.name}${p.caller ? ` · ${p.caller}` : ""}` }), + el("div", { className: "muted mono", textContent: p.words.join(" ") }), + el("div", { className: "muted", textContent: `Paired ${new Date(p.created_at * 1000).toLocaleString()}` }), + el("button", { + textContent: "Unpair", + onclick: async () => { + await invoke("revoke_pairing", { id: p.id }); + await renderSettings(target, state); + }, + })), + ); + } + if (!pairings.length) list.append(el("li", { className: "muted", textContent: "No browsers paired. Open the Kestrove extension in your browser and choose “Pair”." })); + target.replaceChildren( + el("h2", { textContent: "Settings" }), + el("label", { className: "row" }, "Lock after idle for ", idle), + el("p", { className: "muted", textContent: "Kestrove also locks on screen lock and sleep." }), + el("h3", { textContent: "Paired browsers" }), + list, + ); +} + +// ------------------------------------------------------------------ browser prompts (pairing, save) + +async function renderPrompts(state: AppState) { + overlay.replaceChildren(); + const pairing = await invoke("pending_pairing"); + if (pairing) overlay.append(pairingCard(pairing, state)); + if (state.has_account && !state.locked) { + for (const s of await invoke("pending_saves")) overlay.append(saveCard(s)); + } else if (state.has_account && (await invoke("pending_saves")).length) { + overlay.append(el("div", { className: "prompt" }, el("p", { textContent: "Unlock Kestrove to review logins your browser asked to save." }))); + } + overlay.hidden = !overlay.childElementCount; +} + +function pairingCard(p: PairingView, state: AppState): HTMLElement { + const err = el("div", { className: "err" }); + const words = el("div", { className: "words" }, ...p.words.map((w) => el("span", { textContent: w }))); + const accept = el("button", { textContent: "The words match: pair", className: "primary" }); + const reject = el("button", { textContent: "Don't pair" }); + const resolve = async (yes: boolean) => { + try { + await invoke("resolve_pairing", { id: p.id, accept: yes }); + await render(); + } catch (e) { + err.textContent = errText(e); + } + }; + accept.onclick = () => resolve(true); + reject.onclick = () => resolve(false); + accept.disabled = state.locked; + return el("div", { className: "prompt" }, + el("h3", { textContent: "Pair a browser?" }), + el("p", { textContent: `${p.name}${p.caller ? ` (${p.caller})` : ""} wants to use Kestrove. Check that the extension shows exactly these words:` }), + words, + state.locked ? el("p", { className: "muted", textContent: "Unlock Kestrove first, then confirm." }) : "", + el("div", { className: "row" }, accept, reject), + err); +} + +function saveCard(s: SaveView): HTMLElement { + const err = el("div", { className: "err" }); + const yes = el("button", { textContent: s.update ? "Update password" : "Save", className: "primary" }); + const no = el("button", { textContent: "Not now" }); + const resolve = async (accept: boolean) => { + try { + await invoke("resolve_save", { id: s.id, accept }); + await render(); + } catch (e) { + err.textContent = errText(e); + } + }; + yes.onclick = () => resolve(true); + no.onclick = () => resolve(false); + return el("div", { className: "prompt" }, + el("h3", { textContent: s.update ? "Update saved password?" : "Save this login?" }), + el("p", {}, el("span", { className: "mono", textContent: s.username || "(no username)" }), " on ", el("span", { className: "mono", textContent: s.origin })), + el("div", { className: "row" }, yes, no), + err); +} + +// ------------------------------------------------------------------ wiring + +void listen("pairing-request", () => render()); +void listen("save-request", () => render()); +void listen("locked", () => render()); + +// Activity for the idle timer (Linux has no system idle time without extra deps). +let lastTouch = 0; +for (const ev of ["keydown", "mousedown", "mousemove"]) { + document.addEventListener(ev, () => { + const now = Date.now(); + if (now - lastTouch > 30_000) { + lastTouch = now; + void invoke("touch"); + } + }, { passive: true }); } void render(); diff --git a/src/style.css b/src/style.css index 33653e3..df85120 100644 --- a/src/style.css +++ b/src/style.css @@ -14,3 +14,16 @@ section { padding: 16px; } .err { color: #d33; } .mono { font-family: ui-monospace, monospace; } .toolbar { display: flex; gap: 8px; padding: 8px; } +h3 { margin: 16px 0 8px; } +select { font: inherit; padding: 6px; border-radius: 6px; } +.row { display: flex; gap: 8px; align-items: center; } +.pad { padding: 0 12px; } +.primary { background: #2c6e86; color: white; border-color: transparent; } +.danger { color: #d33; } +ul.plain { padding: 0; } +ul.plain li { list-style: none; padding: 8px 0; border-bottom: 1px solid #8883; display: grid; gap: 4px; justify-items: start; } +#overlay { position: fixed; inset: 0; background: #0006; display: grid; place-content: center; gap: 12px; } +#overlay[hidden] { display: none; } +.prompt { background: Canvas; color: CanvasText; border-radius: 10px; padding: 16px 20px; width: 420px; box-shadow: 0 10px 40px #0005; } +.words { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 12px; } +.words span { font: 600 15px ui-monospace, monospace; padding: 4px 8px; border-radius: 6px; background: #8882; }