app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 97acb226f9ecf55b8fa17854fc746660817a2cef
parent 856a14feb6b5a8d0826c83c1059e065d09a4be87
Author: triesap <tyson@radroots.org>
Date:   Fri, 17 Apr 2026 16:14:54 +0000

app: initialize gpui app shell

- archive the previous egui implementation on legacy/egui
- replace the app repo with the new radroots_app gpui bootstrap
- add the minimal native shell with transparent titlebar and centered placeholder view
- align startup boundaries with the canonical app bootstrap and run flow

Diffstat:
MAGENTS.md | 76+++++++++++++++++++++++++++-------------------------------------------------
MCONTRIBUTING.md | 127++++++++-----------------------------------------------------------------------
MCargo.lock | 7329+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
MCargo.toml | 59++++++++---------------------------------------------------
Dassets/geocoder/.gitkeep | 1-
Dcrates/bridges/android/security/Cargo.toml | 20--------------------
Dcrates/bridges/android/security/src/lib.rs | 9---------
Dcrates/bridges/android/security/src/security.rs | 553-------------------------------------------------------------------------------
Dcrates/bridges/android/security/src/vault.rs | 197-------------------------------------------------------------------------------
Dcrates/bridges/apple/security/Cargo.toml | 19-------------------
Dcrates/bridges/apple/security/build.rs | 164-------------------------------------------------------------------------------
Dcrates/bridges/apple/security/src/lib.rs | 7-------
Dcrates/bridges/apple/security/src/security.rs | 454-------------------------------------------------------------------------------
Dcrates/bridges/apple/security/src/vault.rs | 225-------------------------------------------------------------------------------
Dcrates/launchers/android/Cargo.toml | 35-----------------------------------
Dcrates/launchers/android/src/country_lookup.rs | 179-------------------------------------------------------------------------------
Dcrates/launchers/android/src/lib.rs | 1228-------------------------------------------------------------------------------
Dcrates/launchers/android/src/offline_geocoder.rs | 600-------------------------------------------------------------------------------
Dcrates/launchers/android/src/remote_signer.rs | 471-------------------------------------------------------------------------------
Dcrates/launchers/android/src/reverse_lookup.rs | 113-------------------------------------------------------------------------------
Dcrates/launchers/android/src/storage.rs | 107-------------------------------------------------------------------------------
Dcrates/launchers/desktop/Cargo.toml | 44--------------------------------------------
Dcrates/launchers/desktop/assets/icons/radroots-logo.ico | 0
Dcrates/launchers/desktop/build.rs | 214-------------------------------------------------------------------------------
Dcrates/launchers/desktop/macos/Info.plist | 16----------------
Dcrates/launchers/desktop/src/country_lookup.rs | 187-------------------------------------------------------------------------------
Dcrates/launchers/desktop/src/main.rs | 1279-------------------------------------------------------------------------------
Dcrates/launchers/desktop/src/offline_geocoder.rs | 514-------------------------------------------------------------------------------
Dcrates/launchers/desktop/src/remote_signer.rs | 518-------------------------------------------------------------------------------
Dcrates/launchers/desktop/src/reverse_lookup.rs | 119-------------------------------------------------------------------------------
Dcrates/launchers/ios/Cargo.toml | 33---------------------------------
Dcrates/launchers/ios/src/country_lookup.rs | 190-------------------------------------------------------------------------------
Dcrates/launchers/ios/src/lib.rs | 965-------------------------------------------------------------------------------
Dcrates/launchers/ios/src/offline_geocoder.rs | 516-------------------------------------------------------------------------------
Dcrates/launchers/ios/src/remote_signer.rs | 469-------------------------------------------------------------------------------
Dcrates/launchers/ios/src/reverse_lookup.rs | 122-------------------------------------------------------------------------------
Dcrates/launchers/ios/src/storage.rs | 124-------------------------------------------------------------------------------
Dcrates/launchers/web/Cargo.toml | 32--------------------------------
Dcrates/launchers/web/Trunk.toml | 3---
Dcrates/launchers/web/index.html | 65-----------------------------------------------------------------
Dcrates/launchers/web/src/lib.rs | 760-------------------------------------------------------------------------------
Dcrates/launchers/web/src/main.rs | 5-----
Dcrates/shared/core/Cargo.toml | 23-----------------------
Dcrates/shared/core/src/account_roster.rs | 37-------------------------------------
Dcrates/shared/core/src/home_location_tools/country_lookup.rs | 536-------------------------------------------------------------------------------
Dcrates/shared/core/src/home_location_tools/mod.rs | 101-------------------------------------------------------------------------------
Dcrates/shared/core/src/home_location_tools/reverse_lookup.rs | 440-------------------------------------------------------------------------------
Dcrates/shared/core/src/lib.rs | 2742-------------------------------------------------------------------------------
Dcrates/shared/core/src/location_resolver.rs | 123-------------------------------------------------------------------------------
Dcrates/shared/core/src/offline_geocoder.rs | 237-------------------------------------------------------------------------------
Dcrates/shared/core/src/remote_signer.rs | 29-----------------------------
Dcrates/shared/core/src/secret_keys.rs | 58----------------------------------------------------------
Dcrates/shared/core/src/storage_paths.rs | 122-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/Cargo.toml | 29-----------------------------
Dcrates/shared/remote_signer/src/action.rs | 327-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/src/controller.rs | 852-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/src/custody.rs | 624-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/src/error.rs | 64----------------------------------------------------------------
Dcrates/shared/remote_signer/src/input.rs | 168-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/src/lib.rs | 47-----------------------------------------------
Dcrates/shared/remote_signer/src/protocol.rs | 789-------------------------------------------------------------------------------
Dcrates/shared/remote_signer/src/session.rs | 501-------------------------------------------------------------------------------
Dcrates/shared/test_support/Cargo.toml | 17-----------------
Dcrates/shared/test_support/src/lib.rs | 100-------------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/build.gradle.kts | 32--------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/consumer-rules.pro | 1-
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/settings.gradle.kts | 17-----------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/AndroidManifest.xml | 2--
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeySecurityLevel.kt | 73-------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeystoreSecretStore.kt | 371-------------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecretAccessPolicy.kt | 15---------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityBridge.kt | 197-------------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityError.kt | 23-----------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidStoragePaths.kt | 99-------------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidUserPresenceVerifier.kt | 149-------------------------------------------------------------------------------
Dnative/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/test/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityTests.kt | 175-------------------------------------------------------------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Package.swift | 42------------------------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleKeychainSecretStore.swift | 123-------------------------------------------------------------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretAccessPolicy.swift | 28----------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretKey.swift | 21---------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecurityError.swift | 28----------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleUserPresence.swift | 197-------------------------------------------------------------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurityFFI/RadRootsAppleSecurityFFI.swift | 250-------------------------------------------------------------------------------
Dnative/bridges/apple/security/swift/RadRootsAppleSecurity/Tests/RadRootsAppleSecurityTests/RadRootsAppleSecurityTests.swift | 60------------------------------------------------------------
Dplatforms/android/Scripts/android_toolchain_config.sh | 83-------------------------------------------------------------------------------
Dplatforms/android/Scripts/bootstrap_android_toolchain.sh | 226-------------------------------------------------------------------------------
Dplatforms/android/Scripts/build_rust_android.sh | 65-----------------------------------------------------------------
Dplatforms/android/app/build.gradle.kts | 87-------------------------------------------------------------------------------
Dplatforms/android/app/src/main/AndroidManifest.xml | 27---------------------------
Dplatforms/android/app/src/main/kotlin/org/radroots/app/android/MainActivity.kt | 13-------------
Dplatforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt | 129-------------------------------------------------------------------------------
Dplatforms/android/app/src/main/res/drawable-nodpi/radroots_logo.png | 0
Dplatforms/android/app/src/main/res/values/strings.xml | 4----
Dplatforms/android/app/src/main/res/values/themes.xml | 7-------
Dplatforms/android/build.gradle.kts | 8--------
Dplatforms/android/gradle.properties | 3---
Dplatforms/android/settings.gradle.kts | 22----------------------
Dplatforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h | 1-
Dplatforms/ios/App/Bridge/RadRootsIOSBridge.h | 5-----
Dplatforms/ios/App/RadRootsIOS.entitlements | 10----------
Dplatforms/ios/App/Resources/Info.plist | 74--------------------------------------------------------------------------
Dplatforms/ios/App/Resources/LaunchScreen.storyboard | 37-------------------------------------
Dplatforms/ios/App/Resources/RadRootsIcon-20@2x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-20@3x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-29@2x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-29@3x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-40@2x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-40@3x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-60@2x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-60@3x.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png | 0
Dplatforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png | 0
Dplatforms/ios/App/main.swift | 23-----------------------
Dplatforms/ios/Config/Base.xcconfig | 17-----------------
Dplatforms/ios/Config/Debug.xcconfig | 3---
Dplatforms/ios/Config/Release.xcconfig | 3---
Dplatforms/ios/Scripts/build_rust_ios.sh | 89-------------------------------------------------------------------------------
Dplatforms/ios/Scripts/sync_geocoder_resource.sh | 34----------------------------------
Dplatforms/ios/project.yml | 84-------------------------------------------------------------------------------
Dscripts/build-android-host.sh | 34----------------------------------
Dscripts/build-ios-host.sh | 63---------------------------------------------------------------
Dscripts/check-android-target.sh | 20--------------------
Dscripts/check-ios-target.sh | 58----------------------------------------------------------
Ascripts/check.sh | 8++++++++
Dscripts/run-android-emulator.sh | 131-------------------------------------------------------------------------------
Dscripts/run-ios-simulator.sh | 61-------------------------------------------------------------
Ascripts/run.sh | 8++++++++
Dscripts/verify-approved-test-fixtures.sh | 17-----------------
Dscripts/with-wasm-toolchain.sh | 38--------------------------------------
Asrc/app.rs | 40++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 8++++++++
Asrc/main.rs | 5+++++
Asrc/window.rs | 21+++++++++++++++++++++
136 files changed, 4548 insertions(+), 25035 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -5,22 +5,18 @@ - This file applies to the full repository. - Keep this file concise, durable, and repository-specific. - If a closer directory-level `AGENTS.md` is added later, it overrides this file for that subtree. -- Put detailed procedures, examples, and temporary migration notes in other checked-in docs, not here. ## 2. Repository identity - This repository is the standalone Rad Roots application repository. -- Optimize for durable application structure, explicit boundaries, portability, and clear runtime behavior. -- Treat this as a public open-source application project: commits, docs, and handoff language should read clearly to contributors who only know this repository. -- Preserve the repository’s top-level identity files and keep the workspace easy to understand from the root. +- Treat it as a public open-source repository with a direct GPUI-native application focus. +- Preserve the repository's top-level identity files and keep the root easy to understand without mount-path context. ## 3. Change discipline - Prefer the smallest coherent change that fully addresses the request. -- Do not mix unrelated cleanup, speculative refactors, or roadmap work into the same change. -- Prefer clean target-state changes over temporary compatibility layers unless compatibility is explicitly required. -- Remove obsolete code, dependencies, and scaffolding when they are clearly replaced. -- Do not leave hidden task trackers in source comments, markdown checklists, or stray notes. +- Do not mix unrelated cleanup or speculative refactors into the same change. +- Remove obsolete code and scaffolding when they are clearly replaced. ## 4. Before editing @@ -28,62 +24,44 @@ Before making substantial changes: - Read this file, `README.md`, and `CONTRIBUTING.md`. - Inspect `git status --short` before broad edits, refactors, or file removals. -- Read the current implementation and nearby tests before changing behavior. -- Use checked-in documentation and commands as the source of truth. -- Do not assume contributor-specific local tooling or machine setup beyond what the repository documents. -- Surface blockers early when the task depends on unresolved product decisions, missing prerequisites, or unclear architecture boundaries. +- Read the current implementation before changing behavior. +- Use checked-in commands and docs as the source of truth. ## 5. Validation and command surface - Run validation from the repository root. -- Prefer the narrowest relevant validation for the files or crate being changed. -- Use documented commands first. When no narrower repo-specific command is documented, use standard Cargo commands such as: +- Prefer the narrowest relevant validation first. +- Use documented commands before inventing new ones. +- Current canonical commands are: - `cargo metadata --format-version 1 --no-deps` - - `cargo check` - - targeted `cargo test` - - targeted `cargo run -p radroots_app_desktop` -- If validation cannot be run, report the blocker clearly. - -## 6. Workspace structure - -- Keep the repository root as the workspace root. -- Keep reusable Rust application logic under `crates/shared/`. -- Keep Rust host-integration adapters under `crates/bridges/`. -- Keep runnable Rust targets under `crates/launchers/`. -- Keep reusable platform-native bridge libraries under `native/bridges/`. -- Keep native host projects under `platforms/`. -- Add new crates only when they represent a durable architectural boundary. -- Keep manifests, paths, and crate boundaries simple and intentional. -- Do not reintroduce obsolete framework scaffolding unless the requested change explicitly requires it. + - `cargo check -p radroots_app` + - `cargo test` + - `cargo run -p radroots_app` + - `./scripts/check.sh` + - `./scripts/run.sh` -## 7. Rust engineering rules +## 6. Repository structure -- Use Rust `1.92.0`, edition `2024`, and workspace dependency versions from the root `Cargo.toml`. -- Prefer safe, explicit APIs and avoid `unsafe`. -- Keep state, data flow, and side effects understandable; prefer typed models and explicit transitions over stringly APIs or loosely typed maps. -- Avoid hidden panics in non-test code. -- Keep module layout and manifests clean; remove dead dependencies, dead modules, and unused feature wiring when they are no longer needed. -- Keep code readable and direct; avoid unnecessary abstraction in early-stage application code. -- Add or update deterministic tests when behavior, invariants, parsing, or state transitions change. +- Keep the repository root as the package root. +- Keep the structure minimal until a durable new boundary is required. +- Do not reintroduce deprecated egui-era scaffolding. -## 8. Dependency rules +## 7. Rust engineering rules -- Prefer root workspace dependencies where possible. -- Use canonical upstream crate names in manifests and code. -- Prefer dependency choices that align with the existing Rad Roots Rust ecosystem when practical. -- Introduce new dependencies only when they are justified by a clear product or architectural need. +- Use Rust `1.92.0`, edition `2024`, and safe Rust only. +- Keep state, data flow, and side effects explicit. +- Avoid hidden panics in non-test code. +- Keep code readable and direct. -## 9. Commit and handoff rules +## 8. Commit and handoff rules - Format commits as `<scope>: <imperative summary>`. - Use lowercase scopes. -- Split unrelated changes into separate commits. -- Keep commit messages and handoff summaries clear and standalone. +- Keep handoff summaries clear and standalone. - In handoff, state what changed, what validation ran, and any remaining risks or assumptions. -## 10. Definition of done +## 9. Definition of done - The requested change is implemented. -- Replaced or obsolete scaffolding is removed when no longer needed. +- Obsolete scaffolding is removed when clearly replaced. - Relevant validation ran, or a concrete blocker is reported. -- Any affected documentation or structural context is updated with the code change when necessary. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -1,131 +1,40 @@ # Contributing -Rad Roots is an open-source application. Contributions are welcome, including bug fixes, usability improvements, documentation updates, tests, and new features. +Rad Roots is an open-source application repository. +Contributions are welcome, including bug fixes, UI improvements, tests, and documentation updates. ## Scope -This repository is the standalone Rad Roots application repository. Reusable Rust application logic is organized under `crates/shared/`, Rust host bridges under `crates/bridges/`, runnable Rust targets under `crates/launchers/`, native bridge implementations under `native/bridges/`, and native host projects under `platforms/`. +This repository currently ships a single-package GPUI bootstrap application named `radroots_app`. +Keep the filetree small and direct until a larger application boundary is justified. ## Prerequisites -Install the Rust toolchain used by this repository: +Install the pinned Rust toolchain: ```bash rustup toolchain install 1.92.0 -rustup target add wasm32-unknown-unknown -``` - -Install Trunk for the wasm target: - -```bash -cargo install trunk +rustup override set 1.92.0 ``` -On hosts that will build or run the Android shell, ensure Java 17 or newer is available. The Android scripts bootstrap the local Gradle, SDK, NDK, `cargo-ndk`, and emulator resources into `platforms/android/.tooling` on demand. - -On macOS, ensure the Apple Swift toolchain is available. The desktop target links the shared Apple native security package during build. - Confirm your environment: ```bash cargo --version rustc --version -trunk --version -java --version -``` - -On macOS, also confirm: - -```bash -swift --version -``` - -## Getting Started - -Clone your fork and enter the repository root: - -```bash -git clone https://github.com/<YOUR-USERNAME>/app.git -cd app -``` - -To use the repository-pinned toolchain: - -```bash -rustup override set 1.92.0 ``` ## Development Commands Run these commands from the repository root. -Inspect workspace metadata: - ```bash cargo metadata --format-version 1 --no-deps -``` - -Check the application: - -```bash -cargo check -``` - -Run tests: - -```bash +cargo check -p radroots_app cargo test -``` - -Run the native application: - -```bash -cargo run -p radroots_app_desktop -``` - -Check the Android target: - -```bash -./scripts/check-android-target.sh -``` - -Build the Android host: - -```bash -./scripts/build-android-host.sh -``` - -Run the Android app in the emulator: - -```bash -./scripts/run-android-emulator.sh -``` - -Check the wasm application: - -```bash -./scripts/with-wasm-toolchain.sh env -u NO_COLOR cargo check -p radroots_app_web --target wasm32-unknown-unknown -``` - -Build the wasm application: - -```bash -cd crates/launchers/web -../../../scripts/with-wasm-toolchain.sh env -u NO_COLOR trunk build -``` - -Run the wasm application: - -```bash -cd crates/launchers/web -../../../scripts/with-wasm-toolchain.sh env -u NO_COLOR trunk serve --open -``` - -Test the Apple native security package: - -```bash -cd native/bridges/apple/security/swift/RadRootsAppleSecurity -swift test +cargo run -p radroots_app +./scripts/check.sh +./scripts/run.sh ``` ## Contribution Guidelines @@ -133,12 +42,9 @@ swift test - Keep changes scoped to a single coherent change. - Prefer small, reviewable commits. - Update tests when behavior changes. -- Update documentation when commands, structure, or contributor workflow changes. -- Use repo-relative paths in docs, comments, and contributor-facing text. -- Keep documentation path references relative to this repository root. -- Do not use absolute filesystem paths or home-directory path forms in repository docs. +- Update documentation when commands or structure change. +- Use repository-relative paths in contributor-facing text. - Remove obsolete code and dependencies when they are clearly replaced. -- Use workspace-managed dependency versions from the root `Cargo.toml`. ## Reporting Issues @@ -149,7 +55,6 @@ When reporting a bug, include: - the command you ran - the observed behavior - the expected behavior -- logs, screenshots, or backtraces if available ## Submitting Changes @@ -157,11 +62,3 @@ When reporting a bug, include: 2. Make the smallest coherent update that solves the issue. 3. Run the relevant validation commands from this document. 4. Open a pull request with a clear summary of what changed and how it was verified. - -## Code of Conduct - -Be respectful, direct, and constructive in issues and reviews. - -## License - -By contributing to this repository, you agree that your contributions will be distributed under the repository's license. See [LICENSE](LICENSE). diff --git a/Cargo.lock b/Cargo.lock @@ -3,48 +3,12 @@ version = 4 [[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "accesskit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf203f9d3bd8f29f98833d1fbef628df18f759248a547e7e01cfbf63cda36a99" -dependencies = [ - "enumn", - "serde", -] - -[[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" @@ -53,6 +17,7 @@ dependencies = [ "cfg-if", "cipher", "cpufeatures", + "zeroize", ] [[package]] @@ -62,9 +27,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.4", + "const-random", "once_cell", - "serde", "version_check", "zerocopy", ] @@ -79,62 +43,21 @@ dependencies = [ ] [[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-activity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" -dependencies = [ - "android-properties", - "bitflags 2.11.0", - "cc", - "cesu8", - "jni 0.21.1", - "jni-sys 0.3.0", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.15.1" +name = "aligned" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" dependencies = [ - "android_log-sys", - "env_filter", - "log", + "as-slice", ] [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "aligned-vec" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" dependencies = [ - "libc", + "equator", ] [[package]] @@ -144,30 +67,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arboard" -version = "3.6.1" +name = "ar_archive_writer" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "clipboard-win", - "image", - "log", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "parking_lot", - "percent-encoding", - "windows-sys 0.60.2", - "x11rb", + "object", ] [[package]] -name = "arraydeque" -version = "0.5.1" +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "arrayref" @@ -188,6 +111,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" [[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] name = "ash" version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -197,3845 +129,5042 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.89" +name = "ash-window" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82" dependencies = [ - "proc-macro2", - "quote", - "syn", + "ash", + "raw-window-handle", + "raw-window-metal", ] [[package]] -name = "async-utility" -version = "0.3.1" +name = "ashpd" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", "futures-util", - "gloo-timers", - "tokio", - "wasm-bindgen-futures", + "rand 0.9.4", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.12", + "zbus", ] [[package]] -name = "async-wsocket" -version = "0.13.2" +name = "ashpd" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645" dependencies = [ - "async-utility", - "futures", + "async-fs", + "async-net", + "enumflags2", + "futures-channel", "futures-util", - "js-sys", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-tungstenite", + "rand 0.9.4", + "serde", + "serde_repr", "url", - "wasm-bindgen", - "web-sys", + "zbus", ] [[package]] -name = "atomic-destructor" -version = "0.3.0" +name = "async-broadcast" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] [[package]] -name = "atomic-waker" -version = "1.1.2" +name = "async-channel" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] [[package]] -name = "autocfg" -version = "1.5.0" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] [[package]] -name = "base64" -version = "0.21.7" +name = "async-compression" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] [[package]] -name = "base64" -version = "0.22.1" +name = "async-executor" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.4.1", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] [[package]] -name = "base64ct" -version = "1.8.3" +name = "async-fs" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite 2.6.1", +] [[package]] -name = "bech32" -version = "0.11.1" +name = "async-global-executor" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 2.6.1", + "once_cell", +] [[package]] -name = "bip39" -version = "2.2.2" +name = "async-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "bitcoin_hashes", - "serde", - "unicode-normalization", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", ] [[package]] -name = "bit-set" -version = "0.8.0" +name = "async-lock" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "bit-vec", + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", ] [[package]] -name = "bit-vec" -version = "0.8.0" +name = "async-net" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite 2.6.1", +] [[package]] -name = "bitcoin-io" -version = "0.1.4" +name = "async-process" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite 2.6.1", + "rustix 1.1.4", +] [[package]] -name = "bitcoin_hashes" -version = "0.14.1" +name = "async-recursion" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "bitcoin-io", - "hex-conservative", - "serde", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "async-signal" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] [[package]] -name = "bitflags" -version = "2.11.0" +name = "async-std" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ - "serde_core", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.1", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", ] [[package]] -name = "block" -version = "0.1.6" +name = "async-task" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] -name = "block-buffer" -version = "0.10.4" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "generic-array", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "block-padding" -version = "0.3.3" +name = "async_zip" +version = "0.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ - "generic-array", + "async-compression", + "crc32fast", + "futures-lite 2.6.1", + "pin-project", + "thiserror 1.0.69", ] [[package]] -name = "block2" -version = "0.5.1" +name = "atomic" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] -name = "bumpalo" -version = "3.20.2" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "bytemuck" -version = "1.25.0" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "bytemuck_derive" -version = "1.10.2" +name = "av-scenechange" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" dependencies = [ - "proc-macro2", - "quote", - "syn", + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", ] [[package]] -name = "byteorder" -version = "1.5.0" +name = "av1-grain" +version = "0.2.5" 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.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ - "bitflags 2.11.0", + "anyhow", + "arrayvec", "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", + "nom 8.0.0", + "num-rational", + "v_frame", ] [[package]] -name = "calloop" -version = "0.14.4" +name = "avif-serialize" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" dependencies = [ - "bitflags 2.11.0", - "polling", - "rustix 1.1.4", - "slab", - "tracing", + "arrayvec", ] [[package]] -name = "calloop-wayland-source" -version = "0.3.0" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "calloop-wayland-source" -version = "0.4.1" +name = "bindgen" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "calloop 0.14.4", - "rustix 1.1.4", - "wayland-backend", - "wayland-client", + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex", + "syn 2.0.117", ] [[package]] -name = "cbc" -version = "0.1.2" +name = "bit-set" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "cipher", + "bit-vec", ] [[package]] -name = "cc" -version = "1.2.57" +name = "bit-vec" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] -name = "cesu8" -version = "1.1.0" +name = "bit_field" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] -name = "cfg-if" -version = "1.0.4" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "cfg_aliases" -version = "0.2.1" +name = "bitflags" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] -name = "cgl" -version = "0.3.2" +name = "bitstream-io" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "libc", + "no_std_io2", ] [[package]] -name = "chacha20" -version = "0.9.1" +name = "blade-graphics" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +checksum = "e71cfb73b98eb9f58ee84048aa1bdf4e7497fd20c141b57523499fa066b48fed" dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", + "ash", + "ash-window", + "bitflags 2.11.1", + "bytemuck", + "codespan-reporting", + "glow", + "gpu-alloc", + "gpu-alloc-ash", + "hidden-trait", + "js-sys", + "khronos-egl", + "libloading", + "log", + "mint", + "naga", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "objc2-ui-kit", + "once_cell", + "raw-window-handle", + "slab", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "chacha20poly1305" -version = "0.10.1" +name = "blade-macros" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "chrono" -version = "0.4.44" +name = "blade-util" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "3a6be3a82c001ba7a17b6f8e413ede5d1004e6047213f8efaf0ffc15b5c4904c" dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", + "blade-graphics", + "bytemuck", + "log", + "profiling", ] [[package]] -name = "cipher" -version = "0.4.4" +name = "block" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] -name = "clipboard-win" -version = "5.4.1" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "error-code", + "generic-array", ] [[package]] -name = "codespan-reporting" -version = "0.12.0" +name = "block-padding" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "serde", - "termcolor", - "unicode-width", + "generic-array", ] [[package]] -name = "combine" -version = "4.6.7" +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "bytes", - "memchr", + "objc2", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "crossbeam-utils", + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", ] [[package]] -name = "config" -version = "0.14.1" +name = "bstr" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ - "async-trait", - "convert_case", - "json5", - "nom", - "pathdiff", - "ron", - "rust-ini", + "memchr", "serde", - "serde_json", - "toml", - "yaml-rust2", ] [[package]] -name = "const-random" -version = "0.1.18" +name = "built" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] -name = "const-random-macro" -version = "0.1.16" +name = "bumpalo" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] -name = "convert_case" -version = "0.6.0" +name = "bytemuck" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ - "unicode-segmentation", + "bytemuck_derive", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "bytemuck_derive" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ - "core-foundation-sys", - "libc", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "core-foundation" -version = "0.10.1" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "byteorder-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] -name = "core-graphics" -version = "0.23.2" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types 0.5.0", - "libc", + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", ] [[package]] -name = "core-graphics-types" -version = "0.1.3" +name = "calloop-wayland-source" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", ] [[package]] -name = "core-graphics-types" -version = "0.2.0" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "libc", + "cipher", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "cbindgen" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ - "libc", + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", + "tempfile", + "toml 0.8.23", ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "cc" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ - "cfg-if", + "find-msvc-tools", + "jobserver", + "libc", + "shlex", ] [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "crossbeam-utils", + "nom 7.1.3", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "crunchy" -version = "0.2.4" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "crypto-common" -version = "0.1.7" +name = "cgl" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", + "libc", ] [[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "data-encoding" -version = "2.10.0" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] [[package]] -name = "dbus" -version = "0.9.10" +name = "clang-sys" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ + "glob", "libc", - "libdbus-sys", - "windows-sys 0.59.0", + "libloading", ] [[package]] -name = "dbus-secret-service" -version = "4.1.0" +name = "cocoa" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" dependencies = [ - "dbus", - "openssl", - "zeroize", + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", ] [[package]] -name = "deranged" -version = "0.5.8" +name = "cocoa" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "powerfmt", + "bitflags 2.11.1", + "block", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", ] [[package]] -name = "digest" -version = "0.10.7" +name = "cocoa-foundation" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", ] [[package]] -name = "dispatch" +name = "cocoa-foundation" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] [[package]] -name = "dispatch2" -version = "0.3.1" +name = "codespan-reporting" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", + "serde", + "termcolor", + "unicode-width", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "color_quant" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "command-fds" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b60b5124979fccd9addd89d8b97a1d6eebb4950694520c75ddd722535ea443f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "nix 0.31.2", + "thiserror 2.0.18", ] [[package]] -name = "dlib" -version = "0.5.3" +name = "compression-codecs" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" dependencies = [ - "libloading", + "compression-core", + "deflate64", + "flate2", + "memchr", ] [[package]] -name = "dlv-list" -version = "0.5.2" +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "const-random", + "crossbeam-utils", ] [[package]] -name = "document-features" -version = "0.2.12" +name = "const-random" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ - "litrs", + "const-random-macro", ] [[package]] -name = "downcast-rs" -version = "1.2.1" +name = "const-random-macro" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] [[package]] -name = "dpi" -version = "0.1.2" +name = "convert_case" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] -name = "ecolor" -version = "0.33.3" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "bytemuck", - "emath", - "serde", + "core-foundation-sys", + "libc", ] [[package]] -name = "eframe" -version = "0.33.3" +name = "core-foundation" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "egui-wgpu", - "egui-winit", - "egui_glow", - "glow", - "glutin", - "glutin-winit", - "image", - "js-sys", - "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "parking_lot", - "percent-encoding", - "pollster", - "profiling", - "raw-window-handle", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "web-time", - "wgpu", - "windows-sys 0.61.2", - "winit", + "core-foundation-sys", + "libc", ] [[package]] -name = "egui" -version = "0.33.3" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" -dependencies = [ - "accesskit", - "ahash", - "bitflags 2.11.0", - "emath", - "epaint", - "log", - "nohash-hasher", - "profiling", - "serde", - "smallvec", - "unicode-segmentation", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "egui-wgpu" -version = "0.33.3" +name = "core-graphics" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ - "ahash", - "bytemuck", - "document-features", - "egui", - "epaint", - "log", - "profiling", - "thiserror 2.0.18", - "type-map", - "web-time", - "wgpu", - "winit", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", ] [[package]] -name = "egui-winit" -version = "0.33.3" +name = "core-graphics" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "arboard", - "bytemuck", - "egui", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "profiling", - "raw-window-handle", - "smithay-clipboard", - "web-time", - "webbrowser", - "winit", + "bitflags 2.11.1", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", ] [[package]] -name = "egui_glow" -version = "0.33.3" +name = "core-graphics-helmer-fork" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bytemuck", - "egui", - "glow", - "log", - "memoffset", - "profiling", - "wasm-bindgen", - "web-sys", - "winit", + "bitflags 2.11.1", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", ] [[package]] -name = "either" -version = "1.15.0" +name = "core-graphics-types" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] [[package]] -name = "emath" -version = "0.33.3" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bytemuck", - "serde", + "bitflags 2.11.1", + "core-foundation 0.10.0", + "libc", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "core-graphics2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" dependencies = [ + "bitflags 2.11.1", + "block", "cfg-if", + "core-foundation 0.10.0", + "libc", ] [[package]] -name = "enumn" -version = "0.1.14" +name = "core-text" +version = "21.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" dependencies = [ - "proc-macro2", - "quote", - "syn", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", ] [[package]] -name = "env_filter" -version = "0.1.4" +name = "core-video" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" dependencies = [ - "log", - "regex", + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal", ] [[package]] -name = "epaint" -version = "0.33.3" +name = "core_maths" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" dependencies = [ - "ab_glyph", - "ahash", - "bytemuck", - "ecolor", - "emath", - "epaint_default_fonts", - "log", - "nohash-hasher", - "parking_lot", - "profiling", - "serde", + "libm", ] [[package]] -name = "epaint_default_fonts" -version = "0.33.3" +name = "cosmic-text" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" +dependencies = [ + "bitflags 2.11.1", + "fontdb 0.16.2", + "log", + "rangemap", + "rustc-hash 1.1.0", + "rustybuzz 0.14.1", + "self_cell", + "smol_str", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] [[package]] -name = "errno" -version = "0.3.14" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", - "windows-sys 0.61.2", ] [[package]] -name = "error-code" -version = "3.3.2" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] [[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "fastrand" -version = "2.3.0" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "fax" -version = "0.2.6" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "fax_derive" -version = "0.2.0" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "generic-array", + "rand_core 0.6.4", + "typenum", ] [[package]] -name = "fdeflate" -version = "0.3.7" +name = "ctor" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" dependencies = [ - "simd-adler32", + "ctor-proc-macro", + "dtor", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "ctor-proc-macro" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" [[package]] -name = "flate2" -version = "1.1.9" +name = "data-url" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "crc32fast", - "miniz_oxide", + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "dirs" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] [[package]] -name = "foreign-types" -version = "0.3.2" +name = "dirs" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "foreign-types-shared 0.1.1", + "dirs-sys 0.4.1", ] [[package]] -name = "foreign-types" -version = "0.5.0" +name = "dirs-sys" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ - "foreign-types-macros", - "foreign-types-shared 0.3.1", + "libc", + "redox_users", + "winapi", ] [[package]] -name = "foreign-types-macros" -version = "0.2.3" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "dispatch" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] -name = "foreign-types-shared" +name = "dispatch2" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "percent-encoding", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "futures" -version = "0.3.32" +name = "dlib" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "libloading", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "downcast-rs" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dtor" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" dependencies = [ - "futures-core", - "futures-sink", + "dtor-proc-macro", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "dtor-proc-macro" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055" [[package]] -name = "futures-io" -version = "0.3.32" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "futures-sink" -version = "0.3.32" +name = "dwrote" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "futures-util" -version = "0.3.32" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", + "cc", "memchr", - "pin-project-lite", - "slab", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "typenum", - "version_check", + "cfg-if", ] [[package]] -name = "gethostname" -version = "1.1.0" +name = "endi" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link", -] +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] -name = "getrandom" -version = "0.2.17" +name = "enumflags2" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", + "enumflags2_derive", + "serde", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "getrandom" +name = "equator" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "equator-macro", ] [[package]] -name = "gl_generator" -version = "0.14.0" +name = "equator-macro" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ - "khronos_api", - "log", - "xml-rs", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "gloo-timers" -version = "0.3.0" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "glow" -version = "0.16.0" +name = "erased-serde" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ - "js-sys", + "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 = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", "slotmap", - "wasm-bindgen", - "web-sys", + "tinyvec", + "ttf-parser 0.20.0", ] [[package]] -name = "glutin" -version = "0.32.3" +name = "fontdb" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ - "bitflags 2.11.0", - "cfg_aliases", - "cgl", - "dispatch2", - "glutin_egl_sys", - "glutin_glx_sys", - "glutin_wgl_sys", - "libloading", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "once_cell", - "raw-window-handle", - "wayland-sys", - "windows-sys 0.52.0", - "x11-dl", + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "glutin-winit" -version = "0.5.0" +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.4.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[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", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-ash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a" +dependencies = [ + "ash", + "gpu-alloc-types", + "tinyvec", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "gpui" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "979b45cfa6ec723b6f42330915a1b3769b930d02b2d505f9697f8ca602bee707" +dependencies = [ + "anyhow", + "as-raw-xcb-connection", + "ashpd 0.11.1", + "async-task", + "bindgen", + "blade-graphics", + "blade-macros", + "blade-util", + "block", + "bytemuck", + "calloop", + "calloop-wayland-source", + "cbindgen", + "cocoa 0.26.0", + "cocoa-foundation 0.2.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "core-video", + "cosmic-text", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "filedescriptor", + "flume", + "foreign-types", + "futures", + "gpui-macros", + "gpui_collections", + "gpui_http_client", + "gpui_media", + "gpui_refineable", + "gpui_semantic_version", + "gpui_sum_tree", + "gpui_util", + "gpui_util_macros", + "image", + "inventory", + "itertools 0.14.0", + "libc", + "log", + "lyon", + "metal", + "naga", + "num_cpus", + "objc", + "oo7", + "open", + "parking", + "parking_lot", + "pathfinder_geometry", + "pin-project", + "postage", + "profiling", + "rand 0.9.4", + "raw-window-handle", + "resvg", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "smol", + "stacksafe", + "strum 0.27.2", + "taffy", + "thiserror 2.0.18", + "usvg", + "uuid", + "waker-fn", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-plasma", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-numerics", + "windows-registry 0.5.3", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-font-kit", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb02dd63a2859714ac7b6b476937617c3c744157af1b49f7c904023a79039be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui_collections" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae39dc6d3d201be97e4bc08d96dbef2bc5b5c3d5734e05786e8cc3043342351c" +dependencies = [ + "indexmap", + "rustc-hash 2.1.2", +] + +[[package]] +name = "gpui_derive_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644de174341a87b3478bd65b66bca38af868bcf2b2e865700523734f83cfc664" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "gpui_http_client" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23822b0a6d2c5e6a42507980a0ab3848610ea908942c8ef98187f646f690335e" +dependencies = [ + "anyhow", + "async-compression", + "async-fs", + "bytes", + "derive_more", + "futures", + "gpui_util", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "sha2", + "tempfile", + "url", + "zed-async-tar", + "zed-reqwest", +] + +[[package]] +name = "gpui_media" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "ctor", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "gpui_perf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40a0961dcf598955130e867f4b731150a20546427b41b1a63767c1037a86d77" +dependencies = [ + "gpui_collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258cb099254e9468181aee5614410fba61db4ae115fc1d51b4a0b985f60d6641" +dependencies = [ + "gpui_derive_refineable", +] + +[[package]] +name = "gpui_semantic_version" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201e45eff7b695528fb3af6560a534943fbc2db5323d755b9d198bd743948e35" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "gpui_sum_tree" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f3bedd573fafafa13d1200b356c588cf094fb2786e3684bb3f5ea59b549fa9" +dependencies = [ + "arrayvec", + "log", + "rayon", +] + +[[package]] +name = "gpui_util" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68faea25903ae524de9af83990b9aa51bcbc8dd085929ac0aea7fd41905e05c3" +dependencies = [ + "anyhow", + "async-fs", + "async_zip", + "command-fds", + "dirs 4.0.0", + "dunce", + "futures", + "futures-lite 1.13.0", + "globset", + "gpui_collections", + "itertools 0.14.0", + "libc", + "log", + "nix 0.29.0", + "regex", + "rust-embed", + "schemars", + "serde", + "serde_json", + "serde_json_lenient", + "shlex", + "smol", + "take-until", + "tempfile", + "tendril", + "unicase", + "walkdir", + "which", +] + +[[package]] +name = "gpui_util_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c28f65ef47fb97e21e82fd4dd75ccc2506eda010c846dc8054015ea234f1a22" +dependencies = [ + "gpui_perf", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "grid" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "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", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hidden-trait" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed9e850438ac849bec07e7d09fbe9309cbd396a5988c30b010580ce08860df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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 = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ - "cfg_aliases", - "glutin", - "raw-window-handle", - "winit", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "glutin_egl_sys" -version = "0.7.1" +name = "id-arena" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" -dependencies = [ - "gl_generator", - "windows-sys 0.52.0", -] +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] -name = "glutin_glx_sys" -version = "0.6.1" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "gl_generator", - "x11-dl", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "glutin_wgl_sys" -version = "0.6.1" +name = "idna_adapter" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ - "gl_generator", + "icu_normalizer", + "icu_properties", ] [[package]] -name = "gpu-alloc" -version = "0.6.0" +name = "image" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ - "bitflags 2.11.0", - "gpu-alloc-types", + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", ] [[package]] -name = "gpu-alloc-types" -version = "0.3.0" +name = "image-webp" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ - "bitflags 2.11.0", + "byteorder-lite", + "quick-error", ] [[package]] -name = "gpu-allocator" -version = "0.27.0" +name = "imagesize" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "windows", -] +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] -name = "gpu-descriptor" -version = "0.3.2" +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "bitflags 2.11.0", - "gpu-descriptor-types", - "hashbrown 0.15.5", + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", ] [[package]] -name = "gpu-descriptor-types" -version = "0.2.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "bitflags 2.11.0", + "block-padding", + "generic-array", ] [[package]] -name = "half" -version = "2.7.1" +name = "instant" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", - "crunchy", - "num-traits", - "zerocopy", ] [[package]] -name = "hashbrown" -version = "0.14.5" +name = "interpolate_name" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ - "ahash", - "allocator-api2", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "inventory" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ - "foldhash 0.1.5", + "rustversion", ] [[package]] -name = "hashbrown" +name = "io-surface" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" dependencies = [ - "foldhash 0.2.0", + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", ] [[package]] -name = "hashlink" -version = "0.8.4" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "heck" -version = "0.5.0" +name = "is-docker" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "is-wsl" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] [[package]] -name = "hex" -version = "0.4.3" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] [[package]] -name = "hex-conservative" -version = "0.2.2" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ - "arrayvec", + "either", ] [[package]] -name = "hexf-parse" -version = "0.2.1" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "hmac" -version = "0.12.1" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "digest", + "getrandom 0.3.4", + "libc", ] [[package]] -name = "http" -version = "1.4.0" +name = "js-sys" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ - "bytes", - "itoa", + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", ] [[package]] -name = "httparse" -version = "1.10.1" +name = "khronos-egl" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", +] [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "kurbo" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "arrayvec", + "euclid", + "smallvec", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "kv-log-macro" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" dependencies = [ - "cc", + "log", ] [[package]] -name = "icu_collections" -version = "2.1.1" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", + "spin", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "leak" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" [[package]] -name = "icu_normalizer" -version = "2.1.1" +name = "leaky-cow" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "leak", ] [[package]] -name = "icu_normalizer_data" -version = "2.1.1" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "icu_properties" -version = "2.1.2" +name = "lebe" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] -name = "icu_properties_data" -version = "2.1.2" +name = "libc" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] -name = "icu_provider" -version = "2.1.1" +name = "libfuzzer-sys" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "arbitrary", + "cc", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] [[package]] -name = "idna" -version = "1.1.0" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.4", ] [[package]] -name = "idna_adapter" -version = "1.2.1" +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "image" -version = "0.25.10" +name = "litemap" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "moxcms", - "num-traits", - "png", - "tiff", -] +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] -name = "indexmap" -version = "2.13.0" +name = "lock_api" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "scopeguard", ] [[package]] -name = "inout" -version = "0.1.4" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ - "block-padding", - "generic-array", + "serde_core", + "value-bag", ] [[package]] -name = "instant" -version = "0.1.13" +name = "loop9" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "imgref", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "jni" -version = "0.21.1" +name = "lyon" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.0", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", + "lyon_algorithms", + "lyon_tessellation", ] [[package]] -name = "jni" -version = "0.22.4" +name = "lyon_algorithms" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f" dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", + "lyon_path", + "num-traits", ] [[package]] -name = "jni-macros" -version = "0.22.4" +name = "lyon_geom" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", + "arrayvec", + "euclid", + "num-traits", ] [[package]] -name = "jni-sys" -version = "0.3.0" +name = "lyon_path" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] [[package]] -name = "jni-sys" -version = "0.4.1" +name = "lyon_tessellation" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +checksum = "8e43b7e44161571868f5c931d12583592c223c5583eef86b08aa02b7048a3552" dependencies = [ - "jni-sys-macros", + "float_next_after", + "lyon_path", + "num-traits", ] [[package]] -name = "jni-sys-macros" -version = "0.4.1" +name = "mac" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] -name = "jobserver" -version = "0.1.34" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ - "getrandom 0.3.4", "libc", ] [[package]] -name = "js-sys" -version = "0.3.91" +name = "maybe-rayon" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ - "once_cell", - "wasm-bindgen", + "cfg-if", + "rayon", ] [[package]] -name = "json5" -version = "0.4.1" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "pest", - "pest_derive", - "serde", + "cfg-if", + "digest", ] [[package]] -name = "keyring" -version = "3.6.3" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" -dependencies = [ - "byteorder", - "dbus-secret-service", - "linux-keyutils", - "log", - "openssl", - "security-framework 2.11.1", - "security-framework 3.7.0", - "windows-sys 0.60.2", - "zeroize", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "khronos-egl" -version = "6.0.0" +name = "memmap2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", - "libloading", - "pkg-config", ] [[package]] -name = "khronos_api" -version = "3.1.0" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "metal" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] -name = "libc" -version = "0.2.183" +name = "mime_guess" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] [[package]] -name = "libdbus-sys" -version = "0.2.7" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "libloading" +name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "cfg-if", - "windows-link", + "adler2", + "simd-adler32", ] [[package]] -name = "libm" -version = "0.2.16" +name = "mint" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] -name = "libredox" -version = "0.1.14" +name = "mio" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ - "bitflags 2.11.0", "libc", - "plain", - "redox_syscall 0.7.3", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "libsqlite3-sys" -version = "0.37.0" +name = "moxcms" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ - "cc", - "pkg-config", - "vcpkg", + "num-traits", + "pxfm", ] [[package]] -name = "linux-keyutils" -version = "0.2.4" +name = "naga" +version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ - "bitflags 2.11.0", - "libc", + "arrayvec", + "bit-set", + "bitflags 2.11.1", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.15.5", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "strum 0.26.3", + "thiserror 2.0.18", + "unicode-ident", ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "nanorand" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.17", +] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "new_debug_unreachable" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "litemap" -version = "0.8.1" +name = "nix" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] [[package]] -name = "litrs" -version = "1.0.0" +name = "nix" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "no_std_io2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" dependencies = [ - "scopeguard", + "memchr", ] [[package]] -name = "log" -version = "0.4.29" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] [[package]] -name = "lru" -version = "0.16.3" +name = "noop_proc_macro" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] -name = "malloc_buf" -version = "0.0.6" +name = "ntapi" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ - "libc", + "winapi", ] [[package]] -name = "matchers" -version = "0.2.0" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "regex-automata", + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", ] [[package]] -name = "memchr" -version = "2.8.0" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] [[package]] -name = "memmap2" -version = "0.9.10" +name = "num-bigint-dig" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "libc", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "serde", + "smallvec", + "zeroize", ] [[package]] -name = "memoffset" -version = "0.9.1" +name = "num-complex" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "autocfg", + "num-traits", ] [[package]] -name = "metal" -version = "0.32.0" +name = "num-derive" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "bitflags 2.11.0", - "block", - "core-graphics-types 0.2.0", - "foreign-types 0.5.0", - "log", - "objc", - "paste", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[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" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "adler2", - "simd-adler32", + "num-traits", ] [[package]] -name = "mio" -version = "1.1.1" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "moxcms" -version = "0.8.1" +name = "num-rational" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ + "num-bigint", + "num-integer", "num-traits", - "pxfm", ] [[package]] -name = "naga" -version = "27.0.3" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.16.1", - "hexf-parse", - "indexmap", + "autocfg", "libm", - "log", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "spirv", - "thiserror 2.0.18", - "unicode-ident", ] [[package]] -name = "ndk" -version = "0.9.0" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "bitflags 2.11.0", - "jni-sys 0.3.0", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", + "hermit-abi", + "libc", ] [[package]] -name = "ndk-context" -version = "0.1.1" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] [[package]] -name = "ndk-sys" -version = "0.6.0+11769913" +name = "objc-foundation" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ - "jni-sys 0.3.0", + "block", + "objc", + "objc_id", ] [[package]] -name = "negentropy" -version = "0.5.0" +name = "objc2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] [[package]] -name = "nohash-hasher" -version = "0.2.0" +name = "objc2-app-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] [[package]] -name = "nom" -version = "7.1.3" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "memchr", - "minimal-lexical", + "bitflags 2.11.1", + "dispatch2", + "objc2", ] [[package]] -name = "nostr" -version = "0.44.2" +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "aes", - "base64 0.22.1", - "bech32", - "bip39", - "bitcoin_hashes", - "cbc", - "chacha20", - "chacha20poly1305", - "getrandom 0.2.17", - "hex", - "instant", - "scrypt", - "secp256k1", - "serde", - "serde_json", - "unicode-normalization", - "url", + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", ] [[package]] -name = "nostr-browser-signer" -version = "0.44.2" +name = "objc2-metal" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0430dc4517ea03ec055c256f06326671a214e0dfd561d46bafb224d3d31314" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "js-sys", - "nostr", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "nostr-database" -version = "0.44.0" +name = "objc2-quartz-core" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "lru", - "nostr", - "tokio", + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-metal", ] [[package]] -name = "nostr-gossip" -version = "0.44.0" +name = "objc2-ui-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "nostr", + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] -name = "nostr-relay-pool" -version = "0.44.0" +name = "objc_exception" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "hex", - "lru", - "negentropy", - "nostr", - "nostr-database", - "tokio", - "tracing", + "cc", ] [[package]] -name = "nostr-sdk" -version = "0.44.1" +name = "objc_id" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ - "async-utility", - "nostr", - "nostr-database", - "nostr-gossip", - "nostr-relay-pool", - "tokio", - "tracing", + "objc", ] [[package]] -name = "nu-ansi-term" -version = "0.50.3" +name = "object" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ - "windows-sys 0.61.2", + "memchr", ] [[package]] -name = "num-conv" -version = "0.2.0" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "num-traits" -version = "0.2.19" +name = "oo7" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d" dependencies = [ - "autocfg", - "libm", + "aes", + "ashpd 0.12.3", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite 2.6.1", + "futures-util", + "getrandom 0.3.4", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "rand 0.9.4", + "serde", + "sha2", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", ] [[package]] -name = "num_enum" -version = "0.7.6" +name = "open" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ - "num_enum_derive", - "rustversion", + "is-wsl", + "libc", + "pathdiff", ] [[package]] -name = "num_enum_derive" -version = "0.7.6" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "objc" -version = "0.2.7" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ - "malloc_buf", + "futures-core", + "pin-project-lite", ] [[package]] -name = "objc-sys" -version = "0.3.5" +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] -name = "objc2" -version = "0.5.2" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "objc-sys", - "objc2-encode", + "lock_api", + "parking_lot_core", ] [[package]] -name = "objc2" -version = "0.6.4" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "objc2-encode", + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", ] [[package]] -name = "objc2-app-kit" -version = "0.2.2" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.11.0", - "block2", - "libc", - "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", - "objc2-foundation 0.2.2", - "objc2-quartz-core", -] +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "objc2-app-kit" -version = "0.3.2" +name = "pastey" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", -] +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] -name = "objc2-cloud-kit" -version = "0.2.2" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] -name = "objc2-contacts" -version = "0.2.2" +name = "pathfinder_geometry" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "log", + "pathfinder_simd", ] [[package]] -name = "objc2-core-data" -version = "0.2.2" +name = "pathfinder_simd" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "rustc_version", ] [[package]] -name = "objc2-core-foundation" -version = "0.3.2" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2 0.6.4", + "digest", + "hmac", ] [[package]] -name = "objc2-core-graphics" -version = "0.3.2" +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-io-surface", + "pin-project-internal", ] [[package]] -name = "objc2-core-image" -version = "0.2.2" +name = "pin-project-internal" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "objc2-core-location" -version = "0.2.2" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "objc2-encode" -version = "4.1.0" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "objc2-foundation" -version = "0.2.2" +name = "piper" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ - "bitflags 2.11.0", - "block2", - "dispatch", - "libc", - "objc2 0.5.2", + "atomic-waker", + "fastrand 2.4.1", + "futures-io", ] [[package]] -name = "objc2-foundation" -version = "0.3.2" +name = "pkg-config" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", -] +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "objc2-io-surface" -version = "0.3.2" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.11.0", - "objc2 0.6.4", - "objc2-core-foundation", -] +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] -name = "objc2-link-presentation" -version = "0.2.2" +name = "png" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "png" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", ] [[package]] -name = "objc2-quartz-core" -version = "0.2.2" +name = "polling" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] -name = "objc2-symbols" -version = "0.2.2" +name = "pollster" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" [[package]] -name = "objc2-ui-kit" -version = "0.2.2" +name = "postage" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-image", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster", + "static_assertions", + "thiserror 1.0.69", ] [[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ - "block2", - "objc2 0.5.2", - "objc2-foundation 0.2.2", + "zerovec", ] [[package]] -name = "objc2-user-notifications" -version = "0.2.2" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "bitflags 2.11.0", - "block2", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", + "zerocopy", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "proc-macro-crate" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] [[package]] -name = "openssl" -version = "0.10.76" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", + "proc-macro2", + "quote", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "openssl-src" -version = "300.5.5+3.5.5" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "cc", + "unicode-ident", ] [[package]] -name = "openssl-sys" -version = "0.9.112" +name = "profiling" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", + "profiling-procmacros", ] [[package]] -name = "orbclient" -version = "0.3.51" +name = "profiling-procmacros" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ - "libc", - "libredox", + "quote", + "syn 2.0.117", ] [[package]] -name = "ordered-float" -version = "5.1.0" +name = "psm" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" dependencies = [ - "num-traits", + "ar_archive_writer", + "cc", ] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "pxfm" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] -name = "owned_ttf_parser" -version = "0.25.1" +name = "qoi" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" dependencies = [ - "ttf-parser", + "bytemuck", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "quick-error" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ - "lock_api", - "parking_lot_core", + "memchr", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "quick-xml" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", + "memchr", ] [[package]] -name = "password-hash" -version = "0.5.0" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "paste" -version = "1.0.15" +name = "quinn-proto" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] [[package]] -name = "pathdiff" -version = "0.2.3" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] [[package]] -name = "pbkdf2" -version = "0.12.2" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "digest", - "hmac", + "proc-macro2", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "pest" -version = "2.8.6" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +name = "radroots_app" +version = "0.1.0" dependencies = [ - "pest", - "pest_generator", + "gpui", ] [[package]] -name = "pest_generator" -version = "2.8.6" +name = "rand" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] [[package]] -name = "pest_meta" -version = "2.8.6" +name = "rand" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "pest", - "sha2", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] -name = "pin-project" -version = "1.1.11" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "pin-project-internal", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "pin-project-internal" -version = "1.1.11" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] [[package]] -name = "plain" -version = "0.2.3" +name = "rangemap" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] -name = "png" -version = "0.18.1" +name = "rav1e" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ - "bitflags 2.11.0", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.4", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", ] [[package]] -name = "polling" -version = "3.11.0" +name = "ravif" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", ] [[package]] -name = "pollster" -version = "0.4.0" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] -name = "poly1305" -version = "0.8.0" +name = "raw-window-metal" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1" dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", + "cocoa 0.25.0", + "core-graphics 0.23.2", + "objc", + "raw-window-handle", ] [[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" +name = "rayon" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ - "portable-atomic", + "either", + "rayon-core", ] [[package]] -name = "potential_utf" -version = "0.1.4" +name = "rayon-core" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ - "zerovec", + "crossbeam-deque", + "crossbeam-utils", ] [[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "read-fonts" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ - "zerocopy", + "bytemuck", + "font-types", ] [[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "prettyplease" -version = "0.2.37" +name = "redox_syscall" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "proc-macro2", - "syn", + "bitflags 1.3.2", ] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "toml_edit 0.25.5+spec-1.1.0", + "bitflags 2.11.1", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "redox_syscall" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "unicode-ident", + "bitflags 2.11.1", ] [[package]] -name = "profiling" -version = "1.0.17" +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] [[package]] -name = "pxfm" -version = "0.1.28" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] [[package]] -name = "quick-error" -version = "2.0.1" +name = "ref-cast-impl" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "quick-xml" -version = "0.39.2" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ + "aho-corasick", "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "quote" -version = "1.0.45" +name = "regex-automata" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ - "proc-macro2", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "r-efi" -version = "6.0.0" +name = "resvg" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radroots_app_android" -version = "0.1.0" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" dependencies = [ - "android_logger", - "eframe", "log", - "radroots_app_android_security", - "radroots_app_core", - "radroots_app_remote_signer", - "radroots_app_test_support", - "radroots_geocoder", - "radroots_identity", - "radroots_nostr_accounts", - "radroots_runtime_paths", - "wgpu", - "winit", - "zeroize", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", ] [[package]] -name = "radroots_app_android_security" -version = "0.1.0" +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" dependencies = [ - "jni 0.21.1", - "ndk-context", - "radroots_nostr_accounts", - "radroots_secret_vault", - "zeroize", + "bytemuck", ] [[package]] -name = "radroots_app_apple_security" -version = "0.1.0" +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "radroots_nostr_accounts", - "radroots_secret_vault", - "zeroize", + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "radroots_app_core" -version = "0.1.0" -dependencies = [ - "eframe", - "egui", - "radroots_app_test_support", - "radroots_runtime_paths", - "zeroize", -] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] -name = "radroots_app_desktop" -version = "0.1.0" +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" dependencies = [ - "eframe", - "egui", - "image", - "log", - "objc2-foundation 0.3.2", - "radroots_app_apple_security", - "radroots_app_core", - "radroots_app_remote_signer", - "radroots_app_test_support", - "radroots_geocoder", - "radroots_identity", - "radroots_nostr_accounts", - "radroots_runtime_paths", - "wgpu", - "zeroize", + "rust-embed-impl", + "rust-embed-utils", + "walkdir", ] [[package]] -name = "radroots_app_ios" -version = "0.1.0" +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ - "eframe", - "log", - "radroots_app_apple_security", - "radroots_app_core", - "radroots_app_remote_signer", - "radroots_app_test_support", - "radroots_geocoder", - "radroots_identity", - "radroots_nostr_accounts", - "radroots_runtime_paths", - "wgpu", - "zeroize", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.117", + "walkdir", ] [[package]] -name = "radroots_app_remote_signer" -version = "0.1.0" +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "nostr", - "radroots_app_test_support", - "radroots_identity", - "radroots_nostr", - "radroots_nostr_accounts", - "radroots_nostr_connect", - "serde", - "serde_json", - "tempfile", - "tokio", - "url", + "globset", + "sha2", + "walkdir", ] [[package]] -name = "radroots_app_test_support" -version = "0.1.0" +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "radroots_identity", + "semver", ] [[package]] -name = "radroots_app_web" -version = "0.1.0" +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "eframe", - "js-sys", - "log", - "nostr", - "nostr-browser-signer", - "radroots_app_core", - "radroots_geocoder", - "wasm-bindgen-futures", - "web-sys", - "wgpu", + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] -name = "radroots_core" -version = "0.1.0-alpha.2" +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "rust_decimal", - "rust_decimal_macros", - "serde", + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] -name = "radroots_events" -version = "0.1.0-alpha.2" +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ - "radroots_core", - "serde", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "radroots_geocoder" -version = "0.1.0-alpha.2" +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "rusqlite", - "serde", - "thiserror 1.0.69", + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", ] [[package]] -name = "radroots_identity" -version = "0.1.0-alpha.2" +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "nostr", - "radroots_events", - "radroots_runtime", - "radroots_runtime_paths", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", + "rustls-pki-types", ] [[package]] -name = "radroots_log" -version = "0.1.0-alpha.2" +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "chrono", - "thiserror 1.0.69", - "tracing", - "tracing-appender", - "tracing-subscriber", + "web-time", + "zeroize", ] [[package]] -name = "radroots_nostr" -version = "0.1.0-alpha.2" +name = "rustls-webpki" +version = "0.103.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ - "nostr", - "nostr-sdk", - "radroots_identity", - "serde", - "serde_json", - "thiserror 1.0.69", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "radroots_nostr_accounts" -version = "0.1.0-alpha.2" +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "radroots_identity", - "radroots_nostr_signer", - "radroots_runtime", - "radroots_secret_vault", - "serde", - "serde_json", - "thiserror 1.0.69", - "zeroize", + "bitflags 2.11.1", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring 0.2.0", + "unicode-ccc 0.2.0", + "unicode-properties", + "unicode-script", ] [[package]] -name = "radroots_nostr_connect" -version = "0.1.0-alpha.2" +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "nostr", - "serde", - "serde_json", - "thiserror 1.0.69", - "url", + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring 0.4.0", + "unicode-ccc 0.4.0", + "unicode-properties", + "unicode-script", ] [[package]] -name = "radroots_nostr_signer" -version = "0.1.0-alpha.2" +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 = [ - "hex", - "nostr", - "radroots_identity", - "radroots_nostr", - "radroots_nostr_connect", - "radroots_runtime", - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", - "url", - "uuid", + "winapi-util", ] [[package]] -name = "radroots_protected_store" -version = "0.1.0-alpha.2" +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "chacha20poly1305", - "getrandom 0.2.17", - "radroots_secret_vault", - "serde", - "serde_json", - "zeroize", + "windows-sys 0.61.2", ] [[package]] -name = "radroots_runtime" -version = "0.1.0-alpha.2" +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ - "anyhow", - "chacha20poly1305", - "config", - "getrandom 0.2.17", - "radroots_log", - "radroots_protected_store", - "radroots_runtime_paths", - "radroots_secret_vault", + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", "serde", "serde_json", - "tempfile", - "thiserror 1.0.69", - "tokio", - "toml", - "tracing", - "zeroize", ] [[package]] -name = "radroots_runtime_paths" -version = "0.1.0-alpha.2" +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ - "thiserror 1.0.69", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", ] [[package]] -name = "radroots_secret_vault" -version = "0.1.0-alpha.2" -dependencies = [ - "keyring", -] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] -name = "rand" -version = "0.8.5" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "rand" -version = "0.9.2" +name = "screencapturekit" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "screencapturekit-sys", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "screencapturekit-sys" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "seahash" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] -name = "rand_core" -version = "0.6.4" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "getrandom 0.2.17", + "bitflags 2.11.1", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "getrandom 0.3.4", + "core-foundation-sys", + "libc", ] [[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" +name = "self_cell" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] -name = "redox_syscall" -version = "0.4.1" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "bitflags 2.11.0", + "serde_core", + "serde_derive", ] [[package]] -name = "redox_syscall" -version = "0.7.3" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "bitflags 2.11.0", + "serde_derive", ] [[package]] -name = "regex" -version = "1.12.3" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "regex-automata" -version = "0.4.14" +name = "serde_derive_internals" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "renderdoc-sys" +name = "serde_fmt" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys 0.52.0", + "serde_core", ] [[package]] -name = "ron" -version = "0.8.1" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "base64 0.21.7", - "bitflags 2.11.0", + "indexmap", + "itoa", + "memchr", "serde", - "serde_derive", + "serde_core", + "zmij", ] [[package]] -name = "rsqlite-vfs" -version = "0.1.0" +name = "serde_json_lenient" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", ] [[package]] -name = "rusqlite" -version = "0.39.0" +name = "serde_repr" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ - "bitflags 2.11.0", - "fallible-iterator", - "fallible-streaming-iterator", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "rust-ini" -version = "0.20.0" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "cfg-if", - "ordered-multimap", + "serde", ] [[package]] -name = "rust_decimal" -version = "1.40.0" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "arrayvec", - "num-traits", - "serde", + "serde_core", ] [[package]] -name = "rust_decimal_macros" -version = "1.40.0" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "quote", - "syn", + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] -name = "rustc-hash" -version = "1.1.0" +name = "sha1_smol" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] -name = "rustc-hash" -version = "2.1.1" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "rustix" -version = "0.38.44" +name = "signal-hook-registry" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "rustls" -version = "0.23.37" +name = "simd_helpers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "quote", ] [[package]] -name = "rustls-pki-types" -version = "1.14.0" +name = "simplecss" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ - "zeroize", + "log", ] [[package]] -name = "rustls-webpki" -version = "0.103.10" +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skrifa" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "bytemuck", + "read-fonts", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "salsa20" -version = "0.10.2" +name = "slotmap" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ - "cipher", + "version_check", ] [[package]] -name = "same-file" -version = "1.0.6" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "scoped-tls" -version = "1.0.1" +name = "smol" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite 2.6.1", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "smol_str" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" [[package]] -name = "scrypt" -version = "0.11.0" +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "sctk-adwaita" -version = "0.10.1" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit 0.19.2", - "tiny-skia", + "lock_api", ] [[package]] -name = "secp256k1" -version = "0.29.1" +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "rand 0.8.5", - "secp256k1-sys", - "serde", + "bitflags 2.11.1", ] [[package]] -name = "secp256k1-sys" -version = "0.10.1" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "security-framework" -version = "2.11.1" +name = "stacker" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "core-foundation-sys", + "cc", + "cfg-if", "libc", - "security-framework-sys", + "psm", + "windows-sys 0.59.0", ] [[package]] -name = "security-framework" -version = "3.7.0" +name = "stacksafe" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", + "stacker", + "stacksafe-macro", ] [[package]] -name = "security-framework-sys" -version = "2.17.0" +name = "stacksafe-macro" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" dependencies = [ - "core-foundation-sys", - "libc", + "proc-macro-error2", + "quote", + "syn 2.0.117", ] [[package]] -name = "semver" -version = "1.0.27" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "serde" -version = "1.0.228" +name = "strict-num" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "serde_core", - "serde_derive", + "float-cmp", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "strum" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "serde_derive", + "strum_macros 0.26.4", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "strum" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "proc-macro2", - "quote", - "syn", + "strum_macros 0.27.2", ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "strum_macros" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "strum_macros" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "serde", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "sha1" -version = "0.10.6" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "sha2" -version = "0.10.9" +name = "sval" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] +checksum = "2eb9318255ebd817902d7e279d8f8e39b35b1b9954decd5eb9ea0e30e5fd2b6a" [[package]] -name = "sharded-slab" -version = "0.1.7" +name = "sval_buffer" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +checksum = "12571299185e653fdb0fbfe36cd7f6529d39d4e747a60b15a3f34574b7b97c61" dependencies = [ - "lazy_static", + "sval", + "sval_ref", ] [[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "sval_dynamic" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "39526f24e997706c0de7f03fb7371f7f5638b66a504ded508e20ad173d0a3677" dependencies = [ - "errno", - "libc", + "sval", ] [[package]] -name = "simd-adler32" -version = "0.3.8" +name = "sval_fmt" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "933dd3bb26965d682280fcc49400ac2a05036f4ee1e6dbd61bf8402d5a5c3a54" +dependencies = [ + "itoa", + "ryu", + "sval", +] [[package]] -name = "simd_cesu8" -version = "1.1.1" +name = "sval_json" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "a0cda08f6d5c9948024a6551077557b1fdcc3880ff2f20ae839667d2ec2d87ed" dependencies = [ - "rustc_version", - "simdutf8", + "itoa", + "ryu", + "sval", ] [[package]] -name = "simdutf8" -version = "0.1.5" +name = "sval_nested" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +checksum = "88d49d5e6c1f9fd0e53515819b03a97ca4eb1bff5c8ee097c43391c09ecfb19f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] [[package]] -name = "slab" -version = "0.4.12" +name = "sval_ref" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "14f876c5a78405375b4e19cbb9554407513b59c93dea12dc6a4af4e1d30899ca" +dependencies = [ + "sval", +] [[package]] -name = "slotmap" -version = "1.1.1" +name = "sval_serde" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +checksum = "5f9ccd3b7f7200239a655e517dd3fd48d960b9111ad24bd6a5e055bef17607c7" dependencies = [ - "version_check", + "serde_core", + "sval", + "sval_nested", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "svg_fmt" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" [[package]] -name = "smithay-client-toolkit" -version = "0.19.2" +name = "svgtypes" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ - "bitflags 2.11.0", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", + "kurbo", + "siphasher", ] [[package]] -name = "smithay-client-toolkit" -version = "0.20.0" +name = "swash" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" dependencies = [ - "bitflags 2.11.0", - "calloop 0.14.4", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.4", - "thiserror 2.0.18", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", + "skrifa", + "yazi", + "zeno", ] [[package]] -name = "smithay-clipboard" -version = "0.7.3" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "smol_str" -version = "0.2.2" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "serde", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "socket2" -version = "0.6.3" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "libc", - "windows-sys 0.60.2", + "futures-core", ] [[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "bitflags 2.11.0", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" +name = "sys-locale" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", + "libc", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "sysinfo" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "system-configuration" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] [[package]] -name = "strict-num" -version = "0.1.1" +name = "system-configuration-sys" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "subtle" -version = "2.6.1" +name = "taffy" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004" +dependencies = [ + "arrayvec", + "grid", + "serde", + "slotmap", +] [[package]] -name = "syn" -version = "2.0.117" +name = "take-until" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" [[package]] -name = "synstructure" -version = "0.13.2" +name = "tao-core-video-sys" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cfg-if", + "core-foundation-sys", + "libc", + "objc", ] [[package]] @@ -4044,14 +5173,25 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand", - "getrandom 0.3.4", + "fastrand 2.4.1", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", ] [[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4086,7 +5226,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4097,16 +5237,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", + "syn 2.0.117", ] [[package]] @@ -4124,37 +5255,6 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] name = "tiny-keccak" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4174,6 +5274,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png 0.17.16", "tiny-skia-path", ] @@ -4190,9 +5291,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4215,32 +5316,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2", - "tokio-macros", "windows-sys 0.61.2", ] [[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4263,19 +5351,16 @@ dependencies = [ ] [[package]] -name = "tokio-tungstenite" -version = "0.26.2" +name = "tokio-util" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "futures-util", - "log", - "rustls", - "rustls-pki-types", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", ] [[package]] @@ -4285,12 +5370,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] [[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4301,9 +5401,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.1+spec-1.1.0" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4316,7 +5425,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow 0.7.15", @@ -4324,23 +5433,23 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.5+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime 1.0.1+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.0.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.1", ] [[package]] @@ -4350,26 +5459,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-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 = [ - "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] [[package]] -name = "tracing-appender" -version = "0.2.4" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "crossbeam-channel", - "thiserror 2.0.18", - "time", - "tracing-subscriber", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -4379,71 +5520,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", ] [[package]] -name = "tracing-log" -version = "0.2.0" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tracing-subscriber" -version = "0.3.23" +name = "ttf-parser" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" [[package]] name = "ttf-parser" -version = "0.25.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] -name = "tungstenite" -version = "0.26.2" +name = "ttf-parser" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "rustls", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", + "core_maths", ] [[package]] -name = "type-map" -version = "0.5.1" +name = "typeid" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash 2.1.1", -] +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" @@ -4452,10 +5562,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" @@ -4464,19 +5615,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unicode-normalization" -version = "0.1.25" +name = "unicode-linebreak" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-vo" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" @@ -4491,16 +5657,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[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" @@ -4520,6 +5676,33 @@ dependencies = [ ] [[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb 0.23.0", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree", + "rustybuzz 0.20.1", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4533,26 +5716,63 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", + "sha1_smol", "wasm-bindgen", ] [[package]] -name = "valuable" -version = "0.1.1" +name = "v_frame" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] [[package]] name = "version_check" @@ -4561,6 +5781,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4571,6 +5817,15 @@ dependencies = [ ] [[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4596,9 +5851,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -4609,23 +5864,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4633,22 +5884,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -4676,12 +5927,25 @@ dependencies = [ ] [[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -4689,9 +5953,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -4703,32 +5967,21 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] [[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.11.0", - "cursor-icon", - "wayland-backend", -] - -[[package]] name = "wayland-cursor" -version = "0.31.13" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ "rustix 1.1.4", "wayland-client", @@ -4737,84 +5990,57 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.11" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", "wayland-scanner", ] [[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.11.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.11" +name = "wayland-protocols" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", - "wayland-protocols", "wayland-scanner", ] [[package]] name = "wayland-protocols-plasma" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" -dependencies = [ - "bitflags 2.11.0", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.11" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-scanner", ] [[package]] name = "wayland-scanner" -version = "0.31.9" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.39.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -4824,9 +6050,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4843,281 +6069,266 @@ dependencies = [ ] [[package]] -name = "webbrowser" -version = "1.2.0" +name = "weezl" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" -dependencies = [ - "core-foundation 0.10.1", - "jni 0.22.4", - "log", - "ndk-context", - "objc2 0.6.4", - "objc2-foundation 0.3.2", - "url", - "web-sys", -] +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] -name = "webpki-roots" -version = "0.26.11" +name = "which" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ - "webpki-roots 1.0.6", + "either", + "home", + "rustix 0.38.44", + "winsafe", ] [[package]] -name = "webpki-roots" -version = "1.0.6" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "rustls-pki-types", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "weezl" -version = "0.1.12" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "wgpu" -version = "27.0.1" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "arrayvec", - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "js-sys", - "log", - "naga", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "windows-sys 0.61.2", ] [[package]] -name = "wgpu-core" -version = "27.0.3" +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 = "windows" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.11.0", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.18", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", + "windows-core 0.57.0", + "windows-targets 0.52.6", ] [[package]] -name = "wgpu-core-deps-apple" -version = "27.0.0" +name = "windows" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "wgpu-hal", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", ] [[package]] -name = "wgpu-core-deps-emscripten" -version = "27.0.0" +name = "windows-capture" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ - "wgpu-hal", + "parking_lot", + "rayon", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-future", ] [[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "27.0.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "wgpu-hal", + "windows-core 0.61.2", ] [[package]] -name = "wgpu-hal" -version = "27.0.4" +name = "windows-core" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.11.0", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types 0.2.0", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "once_cell", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.18", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows", - "windows-core", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", ] [[package]] -name = "wgpu-types" -version = "27.0.1" +name = "windows-core" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "bitflags 2.11.0", - "bytemuck", - "js-sys", - "log", - "thiserror 2.0.18", - "web-sys", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] -name = "winapi-util" -version = "0.1.11" +name = "windows-future" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-sys 0.61.2", + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] -name = "windows" -version = "0.58.0" +name = "windows-implement" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ - "windows-core", - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", - "windows-strings", - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-interface" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.6", ] [[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] name = "windows-strings" -version = "0.1.0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link 0.1.3", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] @@ -5153,22 +6364,22 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 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", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -5193,7 +6404,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -5205,10 +6416,19 @@ dependencies = [ ] [[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" @@ -5224,9 +6444,9 @@ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" @@ -5242,9 +6462,9 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" @@ -5272,9 +6492,9 @@ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" @@ -5290,9 +6510,9 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" @@ -5308,9 +6528,9 @@ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" @@ -5326,9 +6546,9 @@ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" @@ -5343,73 +6563,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "winit" -version = "0.30.13" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.11.0", - "block2", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit 0.19.2", - "smol_str", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", + "memchr", ] [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] [[package]] -name = "winnow" -version = "1.0.0" +name = "winreg" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" dependencies = [ - "memchr", + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", ] [[package]] @@ -5428,7 +6621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -5439,10 +6632,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -5458,7 +6651,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -5470,7 +6663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -5502,22 +6695,31 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "x11-dl" +name = "x11" version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" dependencies = [ "libc", - "once_cell", "pkg-config", ] [[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] name = "x11rb" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5526,10 +6728,9 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", - "once_cell", "rustix 1.1.4", "x11rb-protocol", + "xcursor", ] [[package]] @@ -5539,21 +6740,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "xcb" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", + "x11", +] + +[[package]] name = "xcursor" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] -name = "xkbcommon-dl" -version = "0.4.2" +name = "xim-ctext" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" dependencies = [ - "bitflags 2.11.0", - "dlib", - "log", - "once_cell", + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", "xkeysym", ] @@ -5564,27 +6803,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] -name = "xml-rs" -version = "0.8.28" +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" [[package]] -name = "yaml-rust2" -version = "0.8.1" +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", + "dlib", + "once_cell", + "pkg-config", ] [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5593,54 +6844,246 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] [[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite 2.6.1", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zed-async-tar" +version = "0.5.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf4b5f655e29700e473cb1acd914ab112b37b62f96f7e642d5fc6a0c02eb881" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.11.1", + "byteorder", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-reqwest" +version = "0.12.15-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.6", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -5661,14 +7104,14 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5677,9 +7120,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5688,13 +7131,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5710,10 +7153,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] name = "zune-jpeg" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] diff --git a/Cargo.toml b/Cargo.toml @@ -1,18 +1,5 @@ -[workspace] -members = [ - "crates/shared/core", - "crates/shared/remote_signer", - "crates/shared/test_support", - "crates/bridges/android/security", - "crates/bridges/apple/security", - "crates/launchers/android", - "crates/launchers/desktop", - "crates/launchers/ios", - "crates/launchers/web", -] -resolver = "2" - -[workspace.package] +[package] +name = "radroots_app" version = "0.1.0" edition = "2024" authors = ["Radroots Authors"] @@ -21,42 +8,12 @@ license = "GPL-3.0" repository = "https://github.com/radrootslabs/app" homepage = "https://radroots.org" readme = "README.md" +publish = false -[workspace.dependencies] -android_logger = "0.15.1" -eframe = { version = "0.33.3", default-features = false, features = ["default_fonts"] } -egui = { version = "0.33.3", features = ["serde"] } -image = { version = "0.25.10", default-features = false, features = ["ico", "png"] } -jni = "0.21.1" -log = "0.4.28" -ndk-context = "0.1.1" -nostr = { version = "0.44.1", default-features = false, features = ["std"] } -nostr-browser-signer = "0.44.1" -objc2-foundation = { version = "0.3.2", default-features = false, features = ["std"] } -radroots_app_android_security = { path = "crates/bridges/android/security" } -radroots_app_apple_security = { path = "crates/bridges/apple/security" } -radroots_geocoder = { path = "../lib/crates/geocoder" } -radroots_identity = { path = "../lib/crates/identity", default-features = false, features = ["std", "nip49"] } -radroots_nostr = { path = "../lib/crates/nostr", default-features = false, features = ["std", "client"] } -radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", default-features = false, features = ["std", "file-store", "os-keyring"] } -radroots_nostr_connect = { path = "../lib/crates/nostr_connect" } -radroots_runtime_paths = { path = "../lib/crates/runtime_paths" } -radroots_secret_vault = { path = "../lib/crates/secret_vault", default-features = false, features = ["std"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" -tokio = { version = "1.48.0", features = ["rt", "sync", "time"] } -url = "2.5.7" -wasm-bindgen-futures = "0.4.50" -web-sys = { version = "0.3.91", features = ["Document", "HtmlCanvasElement", "Window"] } -wgpu = { version = "27.0.1", default-features = false } -winit = { version = "0.30.13", features = ["android-game-activity"] } -zeroize = "1.8.2" +[workspace] -[workspace.lints.rust] -unsafe_code = "forbid" +[dependencies] +gpui = "0.2.2" -[profile.release] -codegen-units = 1 -lto = true -opt-level = "z" -strip = true +[lints.rust] +unsafe_code = "forbid" diff --git a/assets/geocoder/.gitkeep b/assets/geocoder/.gitkeep @@ -1 +0,0 @@ - diff --git a/crates/bridges/android/security/Cargo.toml b/crates/bridges/android/security/Cargo.toml @@ -1,20 +0,0 @@ -[package] -name = "radroots_app_android_security" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots Android security bridge" -publish = false - -[dependencies] -radroots_nostr_accounts.workspace = true -radroots_secret_vault.workspace = true -zeroize.workspace = true - -[target.'cfg(target_os = "android")'.dependencies] -jni.workspace = true -ndk-context.workspace = true diff --git a/crates/bridges/android/security/src/lib.rs b/crates/bridges/android/security/src/lib.rs @@ -1,9 +0,0 @@ -mod security; -mod vault; - -pub use security::{ - ANDROID_NOSTR_SERVICE, AndroidUserPresenceVerificationResult, - begin_user_presence_verification, is_user_presence_verification_pending, - resolve_radroots_base_root, take_user_presence_verification_result, -}; -pub use vault::RadrootsAndroidKeystoreVault; diff --git a/crates/bridges/android/security/src/security.rs b/crates/bridges/android/security/src/security.rs @@ -1,553 +0,0 @@ -#![cfg_attr(not(target_os = "android"), allow(dead_code))] - -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError; -use std::path::PathBuf; - -pub const ANDROID_NOSTR_SERVICE: &str = "org.radroots.app.nostr"; -pub(crate) const ANDROID_NOSTR_NAMESPACE: &str = "nostr"; - -#[cfg(target_os = "android")] -use jni::objects::{JByteArray, JClass, JObject, JString, JValue}; -#[cfg(target_os = "android")] -use jni::sys::{jboolean, jobject}; -#[cfg(target_os = "android")] -use jni::{JNIEnv, JavaVM}; - -#[cfg(target_os = "android")] -const ANDROID_SECURITY_BRIDGE_CLASS: &str = - "org.radroots.app.android.security.RadRootsAndroidSecurityBridge"; - -#[cfg(target_os = "android")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum AndroidSecretStatus { - Success, - NotFound, - InvalidInput, - Error, -} - -#[cfg(target_os = "android")] -impl AndroidSecretStatus { - fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> { - match value { - 0 => Ok(Self::Success), - 1 => Ok(Self::NotFound), - 2 => Ok(Self::InvalidInput), - 3 => Ok(Self::Error), - other => Err(RadrootsNostrAccountsError::Vault(format!( - "unknown android security bridge status {other}" - ))), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AndroidUserPresenceVerificationResult { - Verified, - Failed(String), -} - -#[cfg(target_os = "android")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum AndroidUserPresenceResultStatus { - None, - Success, - Error, -} - -#[cfg(target_os = "android")] -impl AndroidUserPresenceResultStatus { - fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> { - match value { - 0 => Ok(Self::None), - 1 => Ok(Self::Success), - 2 => Ok(Self::Error), - other => Err(RadrootsNostrAccountsError::Vault(format!( - "unknown android user presence status {other}" - ))), - } - } -} - -#[cfg(target_os = "android")] -pub(crate) fn store_secret( - service: &str, - namespace: &str, - name: &str, - value: &[u8], - device_local_only: bool, - user_presence_required: bool, - prefer_strong_box: bool, -) -> Result<(), RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let service = java_string_arg(&mut env, service)?; - let namespace = java_string_arg(&mut env, namespace)?; - let name = java_string_arg(&mut env, name)?; - let value = env.byte_array_from_slice(value).map_err(jni_error)?; - let value = JObject::from(value); - - let status = env - .call_static_method( - &bridge_class, - "putSecret", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[BZZZ)I", - &[ - JValue::Object(&service), - JValue::Object(&namespace), - JValue::Object(&name), - JValue::Object(&value), - JValue::Bool(bool_to_jboolean(device_local_only)), - JValue::Bool(bool_to_jboolean(user_presence_required)), - JValue::Bool(bool_to_jboolean(prefer_strong_box)), - ], - ) - .and_then(|value| value.i()) - .map_err(jni_error)?; - - match AndroidSecretStatus::from_raw(status)? { - AndroidSecretStatus::Success => Ok(()), - AndroidSecretStatus::NotFound => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge reported not found during store", - )), - AndroidSecretStatus::InvalidInput => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge rejected the store request", - )), - AndroidSecretStatus::Error => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android keystore store failed", - )), - } -} - -#[cfg(not(target_os = "android"))] -pub(crate) fn store_secret( - service: &str, - namespace: &str, - name: &str, - value: &[u8], - device_local_only: bool, - user_presence_required: bool, - prefer_strong_box: bool, -) -> Result<(), RadrootsNostrAccountsError> { - let _ = ( - service, - namespace, - name, - value, - device_local_only, - user_presence_required, - prefer_strong_box, - ); - Err(RadrootsNostrAccountsError::Vault( - "android keystore storage is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub(crate) fn load_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let service = java_string_arg(&mut env, service)?; - let namespace = java_string_arg(&mut env, namespace)?; - let name = java_string_arg(&mut env, name)?; - - let value = env - .call_static_method( - &bridge_class, - "getSecret", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B", - &[ - JValue::Object(&service), - JValue::Object(&namespace), - JValue::Object(&name), - ], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - - if value.is_null() { - let Some(message) = take_last_error_message(&mut env, &bridge_class)? else { - return Ok(None); - }; - return Err(RadrootsNostrAccountsError::Vault(message)); - } - - let value = JByteArray::from(value); - env.convert_byte_array(&value).map(Some).map_err(jni_error) -} - -#[cfg(not(target_os = "android"))] -pub(crate) fn load_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> { - let _ = (service, namespace, name); - Err(RadrootsNostrAccountsError::Vault( - "android keystore storage is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub(crate) fn remove_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let service = java_string_arg(&mut env, service)?; - let namespace = java_string_arg(&mut env, namespace)?; - let name = java_string_arg(&mut env, name)?; - - let status = env - .call_static_method( - &bridge_class, - "deleteSecret", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", - &[ - JValue::Object(&service), - JValue::Object(&namespace), - JValue::Object(&name), - ], - ) - .and_then(|value| value.i()) - .map_err(jni_error)?; - - match AndroidSecretStatus::from_raw(status)? { - AndroidSecretStatus::Success | AndroidSecretStatus::NotFound => Ok(()), - AndroidSecretStatus::InvalidInput => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge rejected the delete request", - )), - AndroidSecretStatus::Error => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android keystore delete failed", - )), - } -} - -#[cfg(not(target_os = "android"))] -pub(crate) fn remove_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let _ = (service, namespace, name); - Err(RadrootsNostrAccountsError::Vault( - "android keystore storage is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub(crate) fn remove_secret_namespace( - service: &str, - namespace: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let service = java_string_arg(&mut env, service)?; - let namespace = java_string_arg(&mut env, namespace)?; - - let status = env - .call_static_method( - &bridge_class, - "deleteSecretNamespace", - "(Ljava/lang/String;Ljava/lang/String;)I", - &[JValue::Object(&service), JValue::Object(&namespace)], - ) - .and_then(|value| value.i()) - .map_err(jni_error)?; - - match AndroidSecretStatus::from_raw(status)? { - AndroidSecretStatus::Success | AndroidSecretStatus::NotFound => Ok(()), - AndroidSecretStatus::InvalidInput => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge rejected the namespace delete request", - )), - AndroidSecretStatus::Error => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android keystore namespace delete failed", - )), - } -} - -#[cfg(not(target_os = "android"))] -pub(crate) fn remove_secret_namespace( - service: &str, - namespace: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let _ = (service, namespace); - Err(RadrootsNostrAccountsError::Vault( - "android keystore storage is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub fn resolve_radroots_base_root() -> Result<PathBuf, RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let value = env - .call_static_method( - &bridge_class, - "resolveRadrootsBaseRoot", - "()Ljava/lang/String;", - &[], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - - if value.is_null() { - return Err(bridge_store_error( - &mut env, - &bridge_class, - "android security bridge returned no storage root", - )); - } - - let value = JString::from(value); - let path: String = env.get_string(&value).map_err(jni_error)?.into(); - Ok(PathBuf::from(path)) -} - -#[cfg(target_os = "android")] -pub fn begin_user_presence_verification( - reason: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - let reason = java_string_arg(&mut env, reason)?; - - let status = env - .call_static_method( - &bridge_class, - "beginUserPresenceVerification", - "(Ljava/lang/String;)I", - &[JValue::Object(&reason)], - ) - .and_then(|value| value.i()) - .map_err(jni_error)?; - - match AndroidSecretStatus::from_raw(status)? { - AndroidSecretStatus::Success => Ok(()), - AndroidSecretStatus::NotFound => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge reported no user presence result", - )), - AndroidSecretStatus::InvalidInput => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android security bridge rejected the user presence request", - )), - AndroidSecretStatus::Error => Err(bridge_vault_error( - &mut env, - &bridge_class, - "android user presence verification failed to start", - )), - } -} - -#[cfg(not(target_os = "android"))] -pub fn begin_user_presence_verification( - reason: &str, -) -> Result<(), RadrootsNostrAccountsError> { - let _ = reason; - Err(RadrootsNostrAccountsError::Vault( - "android user presence verification is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub fn is_user_presence_verification_pending() -> Result<bool, RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - - env.call_static_method( - &bridge_class, - "isUserPresenceVerificationPending", - "()Z", - &[], - ) - .and_then(|value| value.z()) - .map_err(jni_error) -} - -#[cfg(not(target_os = "android"))] -pub fn is_user_presence_verification_pending() -> Result<bool, RadrootsNostrAccountsError> { - Err(RadrootsNostrAccountsError::Vault( - "android user presence verification is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -pub fn take_user_presence_verification_result() --> Result<Option<AndroidUserPresenceVerificationResult>, RadrootsNostrAccountsError> { - let java_vm = android_java_vm()?; - let mut env = java_vm.attach_current_thread().map_err(jni_error)?; - let bridge_class = bridge_class(&mut env)?; - - let status = env - .call_static_method( - &bridge_class, - "takeUserPresenceVerificationResult", - "()I", - &[], - ) - .and_then(|value| value.i()) - .map_err(jni_error)?; - - match AndroidUserPresenceResultStatus::from_raw(status)? { - AndroidUserPresenceResultStatus::None => Ok(None), - AndroidUserPresenceResultStatus::Success => { - Ok(Some(AndroidUserPresenceVerificationResult::Verified)) - } - AndroidUserPresenceResultStatus::Error => { - Ok(Some(AndroidUserPresenceVerificationResult::Failed( - take_last_error_message(&mut env, &bridge_class)? - .unwrap_or_else(|| "android device authentication failed".to_owned()), - ))) - } - } -} - -#[cfg(not(target_os = "android"))] -pub fn take_user_presence_verification_result() --> Result<Option<AndroidUserPresenceVerificationResult>, RadrootsNostrAccountsError> { - Err(RadrootsNostrAccountsError::Vault( - "android user presence verification is only available on android".to_owned(), - )) -} - -#[cfg(not(target_os = "android"))] -#[allow(dead_code)] -pub fn resolve_radroots_base_root() -> Result<PathBuf, RadrootsNostrAccountsError> { - Err(RadrootsNostrAccountsError::Store( - "android mobile base storage root is only available on android".to_owned(), - )) -} - -#[cfg(target_os = "android")] -#[allow(unsafe_code)] -fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { - let context = ndk_context::android_context(); - // SAFETY: ndk_context is initialized by the Android runtime before this code runs and - // returns a stable JavaVM pointer for the current process. - unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(jni_error) -} - -#[cfg(target_os = "android")] -#[allow(unsafe_code)] -fn bridge_class<'local>( - env: &mut JNIEnv<'local>, -) -> Result<JClass<'local>, RadrootsNostrAccountsError> { - let context = ndk_context::android_context(); - // SAFETY: ndk_context stores a live process-wide Context jobject for the active Android app. - let context = unsafe { JObject::from_raw(context.context() as jobject) }; - let context = env.new_local_ref(&context).map_err(jni_error)?; - let class_loader = env - .call_method(&context, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]) - .and_then(|value| value.l()) - .map_err(jni_error)?; - let class_name = env - .new_string(ANDROID_SECURITY_BRIDGE_CLASS) - .map_err(jni_error)?; - let class_name = JObject::from(class_name); - let bridge_class = env - .call_method( - &class_loader, - "loadClass", - "(Ljava/lang/String;)Ljava/lang/Class;", - &[JValue::Object(&class_name)], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - Ok(JClass::from(bridge_class)) -} - -#[cfg(target_os = "android")] -fn java_string_arg<'local>( - env: &mut JNIEnv<'local>, - value: &str, -) -> Result<JObject<'local>, RadrootsNostrAccountsError> { - env.new_string(value).map(JObject::from).map_err(jni_error) -} - -#[cfg(target_os = "android")] -fn take_last_error_message( - env: &mut JNIEnv<'_>, - bridge_class: &JClass<'_>, -) -> Result<Option<String>, RadrootsNostrAccountsError> { - let value = env - .call_static_method( - bridge_class, - "takeLastErrorMessage", - "()Ljava/lang/String;", - &[], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - if value.is_null() { - return Ok(None); - } - let value = JString::from(value); - let value: String = env.get_string(&value).map_err(jni_error)?.into(); - Ok(Some(value)) -} - -#[cfg(target_os = "android")] -fn bridge_vault_error( - env: &mut JNIEnv<'_>, - bridge_class: &JClass<'_>, - fallback: &str, -) -> RadrootsNostrAccountsError { - let message = take_last_error_message(env, bridge_class) - .ok() - .flatten() - .unwrap_or_else(|| fallback.to_owned()); - RadrootsNostrAccountsError::Vault(message) -} - -#[cfg(target_os = "android")] -fn bridge_store_error( - env: &mut JNIEnv<'_>, - bridge_class: &JClass<'_>, - fallback: &str, -) -> RadrootsNostrAccountsError { - let message = take_last_error_message(env, bridge_class) - .ok() - .flatten() - .unwrap_or_else(|| fallback.to_owned()); - RadrootsNostrAccountsError::Store(message) -} - -#[cfg(target_os = "android")] -fn jni_error(error: jni::errors::Error) -> RadrootsNostrAccountsError { - RadrootsNostrAccountsError::Vault(format!("android jni error: {error}")) -} - -#[cfg(target_os = "android")] -fn bool_to_jboolean(value: bool) -> jboolean { - if value { 1 } else { 0 } -} diff --git a/crates/bridges/android/security/src/vault.rs b/crates/bridges/android/security/src/vault.rs @@ -1,197 +0,0 @@ -use crate::security::{ - ANDROID_NOSTR_NAMESPACE, load_secret, remove_secret, remove_secret_namespace, store_secret, -}; -use radroots_secret_vault::{ - RadrootsHostVaultCapabilities, RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, - RadrootsHostVaultResidency, RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault, - RadrootsSecretVaultAccessError, -}; -use zeroize::Zeroizing; - -#[derive(Debug, Clone)] -pub struct RadrootsAndroidKeystoreVault { - service_name: String, - namespace: String, -} - -impl RadrootsAndroidKeystoreVault { - #[must_use] - pub fn new(service_name: impl Into<String>) -> Self { - Self::new_with_namespace(service_name, ANDROID_NOSTR_NAMESPACE) - } - - #[must_use] - pub fn new_with_namespace( - service_name: impl Into<String>, - namespace: impl Into<String>, - ) -> Self { - Self { - service_name: service_name.into(), - namespace: namespace.into(), - } - } - - #[must_use] - pub const fn secure_local_policy() -> RadrootsHostVaultPolicy { - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, - hardware: RadrootsHostVaultHardwarePolicy::PreferHardwareBacked, - } - } - - fn capabilities() -> RadrootsHostVaultCapabilities { - #[cfg(target_os = "android")] - { - RadrootsHostVaultCapabilities { - available: true, - supports_device_local_only: true, - supports_user_presence: true, - supports_hardware_backed: true, - } - } - - #[cfg(not(target_os = "android"))] - { - RadrootsHostVaultCapabilities::unavailable() - } - } - - fn validate_policy( - policy: RadrootsHostVaultPolicy, - ) -> Result<(), RadrootsSecretVaultAccessError> { - Self::capabilities() - .validate(policy) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } - - fn security_flags(policy: RadrootsHostVaultPolicy) -> (bool, bool, bool) { - ( - matches!( - policy.residency, - RadrootsHostVaultResidency::DeviceLocalOnly - ), - matches!( - policy.user_presence, - RadrootsHostVaultUserPresencePolicy::Required - ), - !matches!(policy.hardware, RadrootsHostVaultHardwarePolicy::Any), - ) - } - - pub fn store_secret_with_policy( - &self, - slot: &str, - secret: &str, - policy: RadrootsHostVaultPolicy, - ) -> Result<(), RadrootsSecretVaultAccessError> { - Self::validate_policy(policy)?; - let secret = Zeroizing::new(secret.to_owned()); - let (device_local_only, user_presence_required, prefer_strong_box) = - Self::security_flags(policy); - store_secret( - self.service_name.as_str(), - self.namespace.as_str(), - slot, - secret.as_bytes(), - device_local_only, - user_presence_required, - prefer_strong_box, - ) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } - - #[cfg_attr(not(target_os = "android"), allow(dead_code))] - pub fn purge_namespace(&self) -> Result<(), RadrootsSecretVaultAccessError> { - remove_secret_namespace(self.service_name.as_str(), self.namespace.as_str()) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } -} - -impl RadrootsSecretVault for RadrootsAndroidKeystoreVault { - fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> { - self.store_secret_with_policy(slot, secret, Self::secure_local_policy()) - } - - fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> { - let Some(secret) = - load_secret(self.service_name.as_str(), self.namespace.as_str(), slot) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))? - else { - return Ok(None); - }; - - let secret = Zeroizing::new(secret); - let secret = std::str::from_utf8(secret.as_slice()).map_err(|source| { - RadrootsSecretVaultAccessError::Backend(format!( - "android keystore secret was not valid utf-8: {source}" - )) - })?; - Ok(Some(secret.to_owned())) - } - - fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> { - remove_secret(self.service_name.as_str(), self.namespace.as_str(), slot) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_secret_vault::{ - RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency, - RadrootsHostVaultUserPresencePolicy, - }; - - #[test] - fn secure_local_policy_prefers_device_local_hardware_backed_storage() { - assert_eq!( - RadrootsAndroidKeystoreVault::secure_local_policy(), - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, - hardware: RadrootsHostVaultHardwarePolicy::PreferHardwareBacked, - } - ); - } - - #[test] - fn security_flags_request_strong_box_for_hardware_backed_policies() { - assert_eq!( - RadrootsAndroidKeystoreVault::security_flags(RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::UserProfile, - user_presence: RadrootsHostVaultUserPresencePolicy::Required, - hardware: RadrootsHostVaultHardwarePolicy::Any, - }), - (false, true, false) - ); - assert_eq!( - RadrootsAndroidKeystoreVault::security_flags(RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::Required, - hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked, - }), - (true, true, true) - ); - } - - #[cfg(not(target_os = "android"))] - #[test] - fn vault_operations_report_unavailable_off_android() { - let vault = RadrootsAndroidKeystoreVault::new(crate::security::ANDROID_NOSTR_SERVICE); - - let load = vault.load_secret("alice").expect_err("load off android"); - assert!(load.to_string().starts_with("secret vault access error:")); - - let store = vault - .store_secret("alice", "deadbeef") - .expect_err("store off android"); - assert!(store.to_string().starts_with("secret vault access error:")); - - let remove = vault - .remove_secret("alice") - .expect_err("remove off android"); - assert!(remove.to_string().starts_with("secret vault access error:")); - } -} diff --git a/crates/bridges/apple/security/Cargo.toml b/crates/bridges/apple/security/Cargo.toml @@ -1,19 +0,0 @@ -[package] -name = "radroots_app_apple_security" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots Apple security bridge" -publish = false - -[dependencies] -radroots_nostr_accounts.workspace = true -radroots_secret_vault.workspace = true -zeroize.workspace = true - -[lints.rust] -unsafe_code = { level = "allow", priority = 1 } diff --git a/crates/bridges/apple/security/build.rs b/crates/bridges/apple/security/build.rs @@ -1,164 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - - let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if target_os != "macos" && target_os != "ios" { - return; - } - - let package_dir = swift_package_dir(); - println!( - "cargo:rerun-if-changed={}", - package_dir.join("Package.swift").display() - ); - println!( - "cargo:rerun-if-changed={}", - package_dir.join("Sources").display() - ); - println!( - "cargo:rerun-if-changed={}", - package_dir.join("Tests").display() - ); - - let ffi_library = "libRadRootsAppleSecurityFFIDynamic.dylib"; - run_swift_build(package_dir.as_path(), "RadRootsAppleSecurityFFIDynamic"); - - let build_dir = find_library_dir(package_dir.join(".build"), ffi_library) - .expect("swift ffi library dir"); - let copied_library_dir = target_profile_dir(); - fs::create_dir_all(&copied_library_dir).expect("create target profile dir"); - fs::copy( - build_dir.join(ffi_library), - copied_library_dir.join(ffi_library), - ) - .expect("copy swift ffi library into cargo target dir"); - let swift_runtime_dir = swift_runtime_dir(target_os.as_str()); - println!( - "cargo:rustc-link-search=native={}", - copied_library_dir.display() - ); - println!( - "cargo:rustc-link-search=native={}", - swift_runtime_dir.display() - ); - println!( - "cargo:rustc-link-arg=-Wl,-rpath,{}", - copied_library_dir.display() - ); - println!( - "cargo:rustc-link-arg=-Wl,-rpath,{}", - swift_runtime_dir.display() - ); - println!("cargo:rustc-link-lib=dylib=RadRootsAppleSecurityFFIDynamic"); - println!("cargo:rustc-link-lib=framework=CoreFoundation"); - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=LocalAuthentication"); - println!("cargo:rustc-link-lib=framework=Security"); - println!("cargo:rustc-link-lib=dylib=objc"); -} - -fn swift_package_dir() -> PathBuf { - PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir")) - .join("../../../../native/bridges/apple/security/swift/RadRootsAppleSecurity") -} - -fn swift_runtime_dir(target_os: &str) -> PathBuf { - let swift_bin = run_stdout(Command::new("xcrun").arg("--toolchain").arg("swift").arg("--find").arg("swift")); - let swift_bin = PathBuf::from(swift_bin.trim()); - let toolchain_dir = swift_bin - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .expect("swift toolchain dir"); - find_swift_runtime_dir(toolchain_dir.join("usr/lib"), target_os).expect("swift runtime dir") -} - -fn find_swift_runtime_dir(root: PathBuf, target_os: &str) -> Option<PathBuf> { - let platform_dir = match target_os { - "macos" => "macosx", - "ios" => "iphoneos", - other => other, - }; - let mut stack = vec![root]; - while let Some(dir) = stack.pop() { - let entries = std::fs::read_dir(&dir).ok()?; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - continue; - } - if path.file_name().is_some_and(|name| name == "libswift_Concurrency.dylib") - && path - .components() - .any(|component| component.as_os_str() == platform_dir) - { - return path.parent().map(Path::to_path_buf); - } - } - } - None -} - -fn find_library_dir(root: PathBuf, library_name: &str) -> Option<PathBuf> { - let mut stack = vec![root]; - while let Some(dir) = stack.pop() { - let entries = std::fs::read_dir(&dir).ok()?; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - continue; - } - if path.file_name().is_some_and(|name| name == library_name) { - return path.parent().map(Path::to_path_buf); - } - } - } - None -} - -fn target_profile_dir() -> PathBuf { - let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); - out_dir - .ancestors() - .nth(3) - .unwrap_or_else(|| panic!("unexpected cargo OUT_DIR layout: {}", out_dir.display())) - .to_path_buf() -} - -fn run_swift_build(package_dir: &Path, product: &str) { - let output = Command::new("swift") - .arg("build") - .arg("--product") - .arg(product) - .current_dir(package_dir) - .output() - .expect("failed to run swift build"); - - if output.status.success() { - return; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - panic!( - "swift build --product {product} failed in {}:\nstdout:\n{stdout}\nstderr:\n{stderr}", - package_dir.display() - ); -} - -fn run_stdout(command: &mut Command) -> String { - let output = command.output().expect("failed to run command"); - if output.status.success() { - return String::from_utf8(output.stdout).expect("utf-8 stdout"); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - panic!("command failed: {command:?}\nstderr:\n{stderr}"); -} diff --git a/crates/bridges/apple/security/src/lib.rs b/crates/bridges/apple/security/src/lib.rs @@ -1,7 +0,0 @@ -#![allow(unsafe_code)] - -mod security; -mod vault; - -pub use security::{APPLE_NOSTR_NAMESPACE, APPLE_NOSTR_SERVICE, verify_user_presence}; -pub use vault::RadrootsAppleKeychainVault; diff --git a/crates/bridges/apple/security/src/security.rs b/crates/bridges/apple/security/src/security.rs @@ -1,454 +0,0 @@ -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError; -#[cfg(any(target_os = "ios", target_os = "macos"))] -use std::ffi::CStr; -use std::ffi::CString; -#[cfg(any(target_os = "ios", target_os = "macos"))] -use std::os::raw::{c_char, c_int}; -#[cfg(any(target_os = "ios", target_os = "macos"))] -use std::ptr; - -pub const APPLE_NOSTR_SERVICE: &str = "org.radroots.app.nostr"; -pub const APPLE_NOSTR_NAMESPACE: &str = "nostr"; - -#[cfg(any(target_os = "ios", target_os = "macos"))] -#[repr(i32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum AppleSecretStatus { - Success = 0, - NotFound = 1, - InvalidInput = 2, - Error = 3, -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -impl AppleSecretStatus { - fn from_raw(value: i32) -> Result<Self, RadrootsNostrAccountsError> { - match value { - 0 => Ok(Self::Success), - 1 => Ok(Self::NotFound), - 2 => Ok(Self::InvalidInput), - 3 => Ok(Self::Error), - other => Err(RadrootsNostrAccountsError::Vault(format!( - "unknown apple security ffi status {other}" - ))), - } - } -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -unsafe extern "C" { - fn radroots_apple_secret_store_put( - service_prefix: *const c_char, - namespace: *const c_char, - name: *const c_char, - value_ptr: *const u8, - value_len: isize, - accessibility_raw: i32, - device_local_only_raw: i32, - user_presence_required_raw: i32, - error_out: *mut *mut c_char, - ) -> i32; - - fn radroots_apple_secret_store_get( - service_prefix: *const c_char, - namespace: *const c_char, - name: *const c_char, - value_out: *mut *mut u8, - value_len_out: *mut isize, - error_out: *mut *mut c_char, - ) -> i32; - - fn radroots_apple_secret_store_delete( - service_prefix: *const c_char, - namespace: *const c_char, - name: *const c_char, - error_out: *mut *mut c_char, - ) -> i32; - - fn radroots_apple_secret_store_delete_namespace( - service_prefix: *const c_char, - namespace: *const c_char, - error_out: *mut *mut c_char, - ) -> i32; - - fn radroots_apple_user_presence_verify( - reason: *const c_char, - error_out: *mut *mut c_char, - ) -> i32; - - fn radroots_apple_buffer_free(buffer: *mut u8, length: isize); - fn radroots_apple_c_string_free(string: *mut c_char); -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -struct FfiErrorString { - ptr: *mut c_char, -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -impl FfiErrorString { - fn new() -> Self { - Self { - ptr: ptr::null_mut(), - } - } - - fn as_mut_ptr(&mut self) -> *mut *mut c_char { - &mut self.ptr - } - - fn message(&self) -> Option<String> { - if self.ptr.is_null() { - return None; - } - // SAFETY: the Swift FFI returns a null-terminated string pointer that remains valid - // until released through the paired free function. - unsafe { Some(CStr::from_ptr(self.ptr).to_string_lossy().into_owned()) } - } -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -impl Drop for FfiErrorString { - fn drop(&mut self) { - if self.ptr.is_null() { - return; - } - // SAFETY: the pointer originated from the Swift FFI string allocator. - unsafe { - radroots_apple_c_string_free(self.ptr); - } - } -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -struct FfiDataBuffer { - ptr: *mut u8, - len: isize, -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -impl FfiDataBuffer { - fn new() -> Self { - Self { - ptr: ptr::null_mut(), - len: 0, - } - } - - fn as_mut_ptr(&mut self) -> *mut *mut u8 { - &mut self.ptr - } - - fn len_mut_ptr(&mut self) -> *mut isize { - &mut self.len - } - - fn to_vec(&self) -> Result<Vec<u8>, RadrootsNostrAccountsError> { - if self.len < 0 { - return Err(RadrootsNostrAccountsError::Vault( - "apple security ffi returned a negative buffer length".to_owned(), - )); - } - if self.ptr.is_null() { - if self.len == 0 { - return Ok(Vec::new()); - } - return Err(RadrootsNostrAccountsError::Vault( - "apple security ffi returned a null buffer pointer".to_owned(), - )); - } - // SAFETY: the pointer and length pair came from the Swift FFI and stays valid until - // released by the paired free function. We copy into an owned Vec before dropping. - unsafe { Ok(std::slice::from_raw_parts(self.ptr, self.len as usize).to_vec()) } - } -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -impl Drop for FfiDataBuffer { - fn drop(&mut self) { - if self.ptr.is_null() { - return; - } - // SAFETY: the pointer originated from the Swift FFI buffer allocator. - unsafe { - radroots_apple_buffer_free(self.ptr, self.len); - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum AppleSecretAccessibility { - WhenUnlocked = 0, -} - -#[derive(Debug, Clone, Copy)] -pub struct AppleSecretAccessPolicy { - pub accessibility: AppleSecretAccessibility, - pub device_local_only: bool, - pub user_presence_required: bool, -} - -impl AppleSecretAccessPolicy { - #[cfg_attr(not(test), allow(dead_code))] - pub const SECURE_LOCAL_SECRET: Self = Self { - accessibility: AppleSecretAccessibility::WhenUnlocked, - device_local_only: true, - user_presence_required: false, - }; -} - -pub fn store_secret( - service: &str, - namespace: &str, - name: &str, - value: &[u8], - policy: AppleSecretAccessPolicy, -) -> Result<(), RadrootsNostrAccountsError> { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - let service = c_string(service)?; - let namespace = c_string(namespace)?; - let name = c_string(name)?; - let mut ffi_error = FfiErrorString::new(); - let status = unsafe { - // SAFETY: all pointers are derived from live CString values and valid slices. - radroots_apple_secret_store_put( - service.as_ptr(), - namespace.as_ptr(), - name.as_ptr(), - value.as_ptr(), - value.len() as isize, - policy.accessibility as i32, - bool_to_c_int(policy.device_local_only), - bool_to_c_int(policy.user_presence_required), - ffi_error.as_mut_ptr(), - ) - }; - return match AppleSecretStatus::from_raw(status)? { - AppleSecretStatus::Success => Ok(()), - AppleSecretStatus::NotFound => Err(vault_error( - ffi_error, - "apple security ffi reported not found during store", - )), - AppleSecretStatus::InvalidInput => Err(vault_error( - ffi_error, - "apple security ffi rejected the store request", - )), - AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain store failed")), - }; - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - let _ = (service, namespace, name, value, policy); - Err(RadrootsNostrAccountsError::Vault( - "apple keychain storage is only available on ios and macos".to_owned(), - )) - } -} - -pub fn load_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<Option<Vec<u8>>, RadrootsNostrAccountsError> { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - let service = c_string(service)?; - let namespace = c_string(namespace)?; - let name = c_string(name)?; - let mut ffi_error = FfiErrorString::new(); - let mut ffi_buffer = FfiDataBuffer::new(); - let status = unsafe { - // SAFETY: all output pointers reference live local storage for the duration - // of the call, and all input strings are backed by live CString values. - radroots_apple_secret_store_get( - service.as_ptr(), - namespace.as_ptr(), - name.as_ptr(), - ffi_buffer.as_mut_ptr(), - ffi_buffer.len_mut_ptr(), - ffi_error.as_mut_ptr(), - ) - }; - return match AppleSecretStatus::from_raw(status)? { - AppleSecretStatus::Success => ffi_buffer.to_vec().map(Some), - AppleSecretStatus::NotFound => Ok(None), - AppleSecretStatus::InvalidInput => Err(vault_error( - ffi_error, - "apple security ffi rejected the load request", - )), - AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain load failed")), - }; - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - let _ = (service, namespace, name); - Err(RadrootsNostrAccountsError::Vault( - "apple keychain storage is only available on ios and macos".to_owned(), - )) - } -} - -pub fn remove_secret( - service: &str, - namespace: &str, - name: &str, -) -> Result<(), RadrootsNostrAccountsError> { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - let service = c_string(service)?; - let namespace = c_string(namespace)?; - let name = c_string(name)?; - let mut ffi_error = FfiErrorString::new(); - let status = unsafe { - // SAFETY: all pointers are backed by live CString values for the duration - // of the call. - radroots_apple_secret_store_delete( - service.as_ptr(), - namespace.as_ptr(), - name.as_ptr(), - ffi_error.as_mut_ptr(), - ) - }; - return match AppleSecretStatus::from_raw(status)? { - AppleSecretStatus::Success | AppleSecretStatus::NotFound => Ok(()), - AppleSecretStatus::InvalidInput => Err(vault_error( - ffi_error, - "apple security ffi rejected the delete request", - )), - AppleSecretStatus::Error => Err(vault_error(ffi_error, "apple keychain delete failed")), - }; - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - let _ = (service, namespace, name); - Err(RadrootsNostrAccountsError::Vault( - "apple keychain storage is only available on ios and macos".to_owned(), - )) - } -} - -pub fn remove_secret_namespace( - service: &str, - namespace: &str, -) -> Result<(), RadrootsNostrAccountsError> { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - let service = c_string(service)?; - let namespace = c_string(namespace)?; - let mut ffi_error = FfiErrorString::new(); - let status = unsafe { - // SAFETY: all pointers are backed by live CString values for the duration - // of the call. - radroots_apple_secret_store_delete_namespace( - service.as_ptr(), - namespace.as_ptr(), - ffi_error.as_mut_ptr(), - ) - }; - return match AppleSecretStatus::from_raw(status)? { - AppleSecretStatus::Success | AppleSecretStatus::NotFound => Ok(()), - AppleSecretStatus::InvalidInput => Err(vault_error( - ffi_error, - "apple security ffi rejected the namespace delete request", - )), - AppleSecretStatus::Error => Err(vault_error( - ffi_error, - "apple keychain namespace delete failed", - )), - }; - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - let _ = (service, namespace); - Err(RadrootsNostrAccountsError::Vault( - "apple keychain storage is only available on ios and macos".to_owned(), - )) - } -} - -pub fn verify_user_presence(reason: &str) -> Result<(), RadrootsNostrAccountsError> { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - let reason = c_string(reason)?; - let mut ffi_error = FfiErrorString::new(); - let status = unsafe { - // SAFETY: the reason pointer is derived from a live CString and the error output - // references live local storage for the duration of the call. - radroots_apple_user_presence_verify(reason.as_ptr(), ffi_error.as_mut_ptr()) - }; - return match AppleSecretStatus::from_raw(status)? { - AppleSecretStatus::Success => Ok(()), - AppleSecretStatus::NotFound => Err(vault_error( - ffi_error, - "apple security ffi reported not found during user presence verification", - )), - AppleSecretStatus::InvalidInput => Err(vault_error( - ffi_error, - "apple security ffi rejected the user presence request", - )), - AppleSecretStatus::Error => Err(vault_error( - ffi_error, - "apple user presence verification failed", - )), - }; - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - let _ = reason; - Err(RadrootsNostrAccountsError::Vault( - "apple user presence verification is only available on ios and macos".to_owned(), - )) - } -} - -fn c_string(value: &str) -> Result<CString, RadrootsNostrAccountsError> { - CString::new(value).map_err(|_| { - RadrootsNostrAccountsError::Vault( - "apple security ffi input contained an interior nul".into(), - ) - }) -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -fn bool_to_c_int(value: bool) -> c_int { - if value { 1 } else { 0 } -} - -#[cfg(any(target_os = "ios", target_os = "macos"))] -fn vault_error( - ffi_error: FfiErrorString, - fallback: impl Into<String>, -) -> RadrootsNostrAccountsError { - let fallback = fallback.into(); - let message = ffi_error.message().unwrap_or(fallback); - RadrootsNostrAccountsError::Vault(message) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn secure_local_secret_policy_defaults_to_when_unlocked_device_local() { - let policy = AppleSecretAccessPolicy::SECURE_LOCAL_SECRET; - - assert!(matches!( - policy.accessibility, - AppleSecretAccessibility::WhenUnlocked - )); - assert!(policy.device_local_only); - assert!(!policy.user_presence_required); - } - - #[test] - fn c_string_rejects_interior_nul() { - let err = c_string("bad\0value").expect_err("interior nul"); - assert!(err.to_string().starts_with("vault error:")); - } -} diff --git a/crates/bridges/apple/security/src/vault.rs b/crates/bridges/apple/security/src/vault.rs @@ -1,225 +0,0 @@ -use crate::security::{ - APPLE_NOSTR_NAMESPACE, AppleSecretAccessPolicy, AppleSecretAccessibility, load_secret, - remove_secret, remove_secret_namespace, store_secret, -}; -use radroots_secret_vault::{ - RadrootsHostVaultCapabilities, RadrootsHostVaultPolicy, RadrootsHostVaultResidency, - RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault, RadrootsSecretVaultAccessError, -}; -use zeroize::Zeroizing; - -#[derive(Debug, Clone)] -pub struct RadrootsAppleKeychainVault { - service_name: String, - namespace: String, - default_policy: RadrootsHostVaultPolicy, -} - -impl RadrootsAppleKeychainVault { - #[must_use] - pub fn new_desktop(service_name: impl Into<String>) -> Self { - Self::new_with_namespace_desktop(service_name, APPLE_NOSTR_NAMESPACE) - } - - #[must_use] - pub fn new_device_local(service_name: impl Into<String>) -> Self { - Self::new_with_namespace_device_local(service_name, APPLE_NOSTR_NAMESPACE) - } - - #[must_use] - pub fn new_with_namespace_desktop( - service_name: impl Into<String>, - namespace: impl Into<String>, - ) -> Self { - Self::new_with_namespace_and_policy(service_name, namespace, Self::desktop_policy()) - } - - #[must_use] - pub fn new_with_namespace_device_local( - service_name: impl Into<String>, - namespace: impl Into<String>, - ) -> Self { - Self::new_with_namespace_and_policy(service_name, namespace, Self::device_local_policy()) - } - - fn new_with_namespace_and_policy( - service_name: impl Into<String>, - namespace: impl Into<String>, - default_policy: RadrootsHostVaultPolicy, - ) -> Self { - Self { - service_name: service_name.into(), - namespace: namespace.into(), - default_policy, - } - } - - #[must_use] - pub const fn desktop_policy() -> RadrootsHostVaultPolicy { - RadrootsHostVaultPolicy::desktop() - } - - #[must_use] - pub const fn device_local_policy() -> RadrootsHostVaultPolicy { - RadrootsHostVaultPolicy::device_local() - } - - fn capabilities() -> RadrootsHostVaultCapabilities { - #[cfg(any(target_os = "ios", target_os = "macos"))] - { - RadrootsHostVaultCapabilities { - available: true, - supports_device_local_only: true, - supports_user_presence: true, - supports_hardware_backed: false, - } - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - { - RadrootsHostVaultCapabilities::unavailable() - } - } - - fn validate_policy( - policy: RadrootsHostVaultPolicy, - ) -> Result<(), RadrootsSecretVaultAccessError> { - Self::capabilities() - .validate(policy) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } - - fn access_policy(policy: RadrootsHostVaultPolicy) -> AppleSecretAccessPolicy { - AppleSecretAccessPolicy { - accessibility: AppleSecretAccessibility::WhenUnlocked, - device_local_only: matches!( - policy.residency, - RadrootsHostVaultResidency::DeviceLocalOnly - ), - user_presence_required: matches!( - policy.user_presence, - RadrootsHostVaultUserPresencePolicy::Required - ), - } - } - - pub fn store_secret_with_policy( - &self, - slot: &str, - secret: &str, - policy: RadrootsHostVaultPolicy, - ) -> Result<(), RadrootsSecretVaultAccessError> { - Self::validate_policy(policy)?; - let secret = Zeroizing::new(secret.to_owned()); - store_secret( - self.service_name.as_str(), - self.namespace.as_str(), - slot, - secret.as_bytes(), - Self::access_policy(policy), - ) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } - - pub fn purge_namespace(&self) -> Result<(), RadrootsSecretVaultAccessError> { - remove_secret_namespace(self.service_name.as_str(), self.namespace.as_str()) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } -} - -impl RadrootsSecretVault for RadrootsAppleKeychainVault { - fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> { - self.store_secret_with_policy(slot, secret, self.default_policy) - } - - fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> { - let Some(secret) = - load_secret(self.service_name.as_str(), self.namespace.as_str(), slot) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string()))? - else { - return Ok(None); - }; - - let secret = Zeroizing::new(secret); - let secret = std::str::from_utf8(secret.as_slice()).map_err(|source| { - RadrootsSecretVaultAccessError::Backend(format!( - "apple keychain secret was not valid utf-8: {source}" - )) - })?; - Ok(Some(secret.to_owned())) - } - - fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> { - remove_secret(self.service_name.as_str(), self.namespace.as_str(), slot) - .map_err(|source| RadrootsSecretVaultAccessError::Backend(source.to_string())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_secret_vault::{ - RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency, - RadrootsHostVaultUserPresencePolicy, - }; - - #[test] - fn desktop_policy_matches_shared_desktop_contract() { - assert_eq!( - RadrootsAppleKeychainVault::desktop_policy(), - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::UserProfile, - user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, - hardware: RadrootsHostVaultHardwarePolicy::Any, - } - ); - } - - #[test] - fn device_local_policy_matches_shared_mobile_contract() { - assert_eq!( - RadrootsAppleKeychainVault::device_local_policy(), - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, - hardware: RadrootsHostVaultHardwarePolicy::Any, - } - ); - } - - #[cfg(not(any(target_os = "ios", target_os = "macos")))] - #[test] - fn vault_operations_report_unavailable_off_apple() { - let vault = RadrootsAppleKeychainVault::new_desktop(crate::APPLE_NOSTR_SERVICE); - - let load = vault.load_secret("alice").expect_err("load off apple"); - assert!(load.to_string().starts_with("secret vault access error:")); - - let store = vault - .store_secret("alice", "deadbeef") - .expect_err("store off apple"); - assert!(store.to_string().starts_with("secret vault access error:")); - - let remove = vault.remove_secret("alice").expect_err("remove off apple"); - assert!(remove.to_string().starts_with("secret vault access error:")); - } - - #[cfg(any(target_os = "ios", target_os = "macos"))] - #[test] - fn hardware_backed_requirement_reports_unsupported() { - let vault = RadrootsAppleKeychainVault::new_device_local(crate::APPLE_NOSTR_SERVICE); - let error = vault - .store_secret_with_policy( - "alice", - "deadbeef", - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::Required, - hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked, - }, - ) - .expect_err("apple adapter should reject hardware-backed requirement"); - - assert!(error.to_string().contains("hardware_backed")); - } -} diff --git a/crates/launchers/android/Cargo.toml b/crates/launchers/android/Cargo.toml @@ -1,35 +0,0 @@ -[package] -name = "radroots_app_android" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots Android launcher" -publish = false - -[lib] -path = "src/lib.rs" -crate-type = ["cdylib", "rlib"] - -[dependencies] -eframe = { workspace = true, features = ["android-game-activity", "glow"] } -log.workspace = true -radroots_app_android_security.workspace = true -radroots_app_core = { path = "../../shared/core" } -radroots_app_remote_signer = { path = "../../shared/remote_signer" } -radroots_geocoder.workspace = true -radroots_identity.workspace = true -radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] } -radroots_runtime_paths.workspace = true -zeroize.workspace = true - -[target.'cfg(target_os = "android")'.dependencies] -android_logger.workspace = true -wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] } -winit.workspace = true - -[dev-dependencies] -radroots_app_test_support = { path = "../../shared/test_support" } diff --git a/crates/launchers/android/src/country_lookup.rs b/crates/launchers/android/src/country_lookup.rs @@ -1,179 +0,0 @@ -#![cfg_attr(not(target_os = "android"), allow(dead_code))] - -#[cfg(target_os = "android")] -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationResolverError, RadrootsOfflineGeocoderState, -}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct AndroidCountryLookup { - country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>, - country_list_changed: Arc<AtomicBool>, - country_list_pending: Arc<AtomicBool>, - country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>, - country_center_changed: Arc<AtomicBool>, - country_center_pending: Arc<AtomicBool>, -} - -impl AndroidCountryLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "android")] - pub(crate) fn begin_list( - &self, - geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_list_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country list query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_list_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_list_result); - let changed = Arc::clone(&self.country_list_changed); - let pending = Arc::clone(&self.country_list_pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::list_countries(&geocoder_state); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "android"))] - pub(crate) fn begin_list( - &self, - _geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - #[cfg(target_os = "android")] - pub(crate) fn begin_center( - &self, - geocoder_state: RadrootsOfflineGeocoderState, - country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_center_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country center query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_center_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_center_result); - let changed = Arc::clone(&self.country_center_changed); - let pending = Arc::clone(&self.country_center_pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::country_center(&geocoder_state, &country_id); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "android"))] - pub(crate) fn begin_center( - &self, - _geocoder_state: RadrootsOfflineGeocoderState, - _country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> { - if !self.country_list_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_list_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "android country list result lock poisoned".to_owned(), - })), - } - } - - pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> { - if !self.country_center_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_center_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "android country center result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint}; - - fn sample_countries() -> RadrootsLocationCountryListResult { - Ok(vec![RadrootsLocationCountry { - country_id: "BR".to_owned(), - country_name: Some("Brazil".to_owned()), - center: RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - }, - }]) - } - - #[test] - fn take_list_update_is_none_until_tracker_changes() { - let tracker = AndroidCountryLookup::new(); - - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_list_update_returns_queued_result_once() { - let tracker = AndroidCountryLookup::new(); - *tracker.country_list_result.lock().unwrap() = Some(sample_countries()); - tracker.country_list_changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_center_update_returns_queued_result_once() { - let tracker = AndroidCountryLookup::new(); - *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - })); - tracker - .country_center_changed - .store(true, Ordering::Release); - - assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235)); - assert_eq!(tracker.take_center_update(), None); - } -} diff --git a/crates/launchers/android/src/lib.rs b/crates/launchers/android/src/lib.rs @@ -1,1228 +0,0 @@ -#[cfg(target_os = "android")] -use android_logger::Config; -#[cfg(target_os = "android")] -use eframe::egui::ViewportBuilder; -#[cfg(target_os = "android")] -use radroots_app_android_security as android_security; -#[cfg(any(target_os = "android", test))] -use radroots_app_core::RadrootsAppBackend; -#[cfg(target_os = "android")] -use radroots_app_core::{APP_NAME, RadrootsApp}; -#[cfg(any(target_os = "android", test))] -use radroots_app_core::{ - HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, ImportActionState, - RadrootsAccountCustody, RadrootsAccountSummary, RadrootsLocationCountry, - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderState, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, - RadrootsSecretImportMode, RadrootsSecretImportRequest, SetupActionState, -}; -#[cfg(any(target_os = "android", test))] -use radroots_identity::RadrootsIdentity; -#[cfg(test)] -use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord; -#[cfg(any(target_os = "android", test))] -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; -#[cfg(any(target_os = "android", test))] -use radroots_nostr_accounts::prelude::RadrootsNostrSelectedAccountStatus; -#[cfg(any(target_os = "android", test))] -use std::path::Path; -#[cfg(any(target_os = "android", test))] -use std::sync::Mutex; -#[cfg(target_os = "android")] -use winit::platform::android::activity::AndroidApp; -#[cfg(any(target_os = "android", test))] -use zeroize::Zeroizing; - -#[cfg(any(target_os = "android", test))] -mod country_lookup; -#[cfg(any(target_os = "android", test))] -mod offline_geocoder; -#[cfg(target_os = "android")] -mod remote_signer; -#[cfg(any(target_os = "android", test))] -mod reverse_lookup; -#[cfg(any(target_os = "android", test))] -mod storage; - -#[cfg(any(target_os = "android", test))] -#[cfg_attr(not(target_os = "android"), allow(dead_code))] -struct AndroidBackend { - country_lookup: country_lookup::AndroidCountryLookup, - offline_geocoder: offline_geocoder::AndroidOfflineGeocoder, - #[cfg(target_os = "android")] - remote_signer: remote_signer::AndroidRemoteSigner, - reverse_lookup: reverse_lookup::AndroidReverseLookup, -} - -#[cfg(any(target_os = "android", test))] -#[cfg_attr(not(target_os = "android"), allow(dead_code))] -enum PendingSecretKeyExport { - EncryptedBackup { password: Zeroizing<String> }, - RawReveal, -} - -#[cfg(any(target_os = "android", test))] -#[cfg_attr(not(target_os = "android"), allow(dead_code))] -static PENDING_SECRET_KEY_EXPORT: Mutex<Option<PendingSecretKeyExport>> = Mutex::new(None); - -#[cfg(any(target_os = "android", test))] -impl RadrootsAppBackend for AndroidBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - #[cfg(target_os = "android")] - { - let manager = Self::accounts_manager()?; - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - return remote_signer::identity_state_from_status(status); - } - - #[cfg(not(target_os = "android"))] - { - Ok(Self::unsupported_identity_state()) - } - } - - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - #[cfg(target_os = "android")] - { - let manager = Self::accounts_manager()?; - return Self::account_roster_from_manager(&manager); - } - - #[cfg(not(target_os = "android"))] - { - Ok(Vec::new()) - } - } - - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - Some(self.offline_geocoder.current_state()) - } - - fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - Ok(self.offline_geocoder.take_update()) - } - - fn reverse_location( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return offline_geocoder::reverse_location( - &self.offline_geocoder.current_state(), - point, - options, - ); - } - - #[cfg(not(target_os = "android"))] - { - let _ = (point, options); - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn request_reverse_location_lookup( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return self.reverse_lookup.begin( - self.offline_geocoder.current_state(), - point, - options, - ); - } - - #[cfg(not(target_os = "android"))] - { - let _ = (point, options); - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(self.reverse_lookup.take_update()) - } - - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return self - .country_lookup - .begin_list(self.offline_geocoder.current_state()); - } - - #[cfg(not(target_os = "android"))] - { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_location_country_list_result( - &self, - ) -> Result<Option<RadrootsLocationCountryListResult>, String> { - Ok(self.country_lookup.take_list_update()) - } - - fn request_location_country_center_lookup( - &self, - country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return self - .country_lookup - .begin_center(self.offline_geocoder.current_state(), country_id.to_owned()); - } - - #[cfg(not(target_os = "android"))] - { - let _ = country_id; - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_location_country_center_lookup_result( - &self, - ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { - Ok(self.country_lookup.take_center_update()) - } - - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return offline_geocoder::list_countries(&self.offline_geocoder.current_state()); - } - - #[cfg(not(target_os = "android"))] - { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn location_country_center( - &self, - country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - #[cfg(target_os = "android")] - { - return offline_geocoder::country_center( - &self.offline_geocoder.current_state(), - country_id, - ); - } - - #[cfg(not(target_os = "android"))] - { - let _ = country_id; - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn setup_action_state(&self) -> SetupActionState { - #[cfg(target_os = "android")] - { - return Self::enabled_setup_action_state(); - } - - #[cfg(not(target_os = "android"))] - { - Self::unsupported_setup_action_state() - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "android")] - { - let manager = Self::accounts_manager()?; - return Self::generate_local_identity(&manager).map(Some); - } - - #[cfg(not(target_os = "android"))] - { - Ok(Some(Self::unsupported_identity_state())) - } - } - - fn home_setup_action_state(&self) -> Option<SetupActionState> { - Some(self.setup_action_state()) - } - - fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - self.request_setup_action() - } - - fn import_action_state(&self) -> Option<ImportActionState> { - #[cfg(target_os = "android")] - { - return Some(ImportActionState { - label: "Import Secret Key".to_owned(), - enabled: true, - pending: false, - }); - } - - #[cfg(not(target_os = "android"))] - { - None - } - } - - fn request_import_action( - &self, - request: &RadrootsSecretImportRequest, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "android")] - { - let manager = Self::accounts_manager()?; - return Self::import_local_identity(&manager, request).map(Some); - } - - #[cfg(not(target_os = "android"))] - { - let _ = request; - Ok(None) - } - } - - fn request_select_account( - &self, - account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "android")] - { - let manager = Self::accounts_manager()?; - let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id) - .map_err(|_| "invalid account id".to_owned())?; - manager - .select_account(&account_id) - .map_err(|source| source.to_string())?; - return self.load_identity_state().map(Some); - } - - #[cfg(not(target_os = "android"))] - { - let _ = account_id; - Ok(None) - } - } - - fn remote_signer_action_state(&self) -> Option<SetupActionState> { - #[cfg(target_os = "android")] - { - return Some( - self.remote_signer - .action_state() - .unwrap_or_else(|_| SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: !self.remote_signer.is_connecting(), - pending: self.remote_signer.is_connecting(), - }), - ); - } - - #[cfg(not(target_os = "android"))] - { - None - } - } - - fn preview_remote_signer_connection( - &self, - input: &str, - ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> { - #[cfg(target_os = "android")] - { - return remote_signer::preview_connection(input); - } - - #[cfg(not(target_os = "android"))] - { - let _ = input; - Err("remote signer onboarding is not available in this build".to_owned()) - } - } - - fn request_remote_signer_connection( - &self, - input: &str, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "android")] - { - self.remote_signer.begin_connect(input)?; - return Ok(None); - } - - #[cfg(not(target_os = "android"))] - { - let _ = input; - Ok(None) - } - } - - fn pending_remote_signer_connection( - &self, - ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> { - #[cfg(target_os = "android")] - { - return self.remote_signer.pending_connection(); - } - - #[cfg(not(target_os = "android"))] - { - Ok(None) - } - } - - fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> { - #[cfg(target_os = "android")] - { - return remote_signer::cancel_pending_connection(); - } - - #[cfg(not(target_os = "android"))] - { - Ok(()) - } - } - - fn remote_signer_note_action_state(&self) -> Option<SetupActionState> { - #[cfg(target_os = "android")] - { - return Some( - self.remote_signer - .note_action_state() - .unwrap_or(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }), - ); - } - - #[cfg(not(target_os = "android"))] - { - None - } - } - - fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> { - #[cfg(target_os = "android")] - { - return self.remote_signer.begin_sign_kind1_note_selected(content); - } - - #[cfg(not(target_os = "android"))] - { - let _ = content; - Ok(()) - } - } - - fn poll_remote_signer_note_action_result( - &self, - ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> { - #[cfg(target_os = "android")] - { - return self - .remote_signer - .take_note_update() - .transpose() - .map(|result| result.flatten()); - } - - #[cfg(not(target_os = "android"))] - { - Ok(None) - } - } - - fn home_action_states(&self) -> Vec<HomeActionState> { - #[cfg(target_os = "android")] - { - let secret_key_export_pending = Self::secret_key_export_pending(); - let Ok(manager) = Self::accounts_manager() else { - return Vec::new(); - }; - let Ok(status) = manager - .selected_account_status() - .map_err(|source| source.to_string()) - else { - return Vec::new(); - }; - - return match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if matches!( - remote_signer::custody_for_account_id(account.account_id.as_str()), - Ok(RadrootsAccountCustody::RemoteSigner) - ) { - vec![HomeActionState { - kind: HomeActionKind::DisconnectSigner, - label: "Disconnect Remote Signer".to_owned(), - enabled: true, - pending: false, - }] - } else { - Vec::new() - } - } - RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![ - HomeActionState { - kind: HomeActionKind::BackupSecretKey, - label: "Back Up Secret Key".to_owned(), - enabled: !secret_key_export_pending, - pending: secret_key_export_pending, - }, - HomeActionState { - kind: HomeActionKind::RevealRawSecretKey, - label: "Reveal Raw Secret Key".to_owned(), - enabled: !secret_key_export_pending, - pending: secret_key_export_pending, - }, - HomeActionState { - kind: HomeActionKind::RemoveLocalKey, - label: "Remove Key From This Device".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::ResetDevice, - label: "Reset This Device".to_owned(), - enabled: true, - pending: false, - }, - ], - }; - } - - #[cfg(not(target_os = "android"))] - { - Vec::new() - } - } - - fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> { - #[cfg(target_os = "android")] - { - return match action { - HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None), - HomeActionKind::RevealRawSecretKey => { - Self::begin_raw_secret_key_reveal().map(|()| HomeActionResult::None) - } - HomeActionKind::RemoveLocalKey => { - let manager = Self::accounts_manager()?; - Self::remove_selected_local_identity(&manager) - .map(HomeActionResult::IdentityState) - } - HomeActionKind::ResetDevice => { - let manager = Self::accounts_manager()?; - let accounts_path = storage::accounts_path()?; - Self::reset_local_device_state(&manager, accounts_path.as_path()) - .map(HomeActionResult::IdentityState) - } - HomeActionKind::DisconnectSigner => { - let manager = Self::accounts_manager()?; - remote_signer::disconnect_selected_remote_signer(&manager) - .map(HomeActionResult::IdentityState) - } - }; - } - - #[cfg(not(target_os = "android"))] - { - let _ = action; - Ok(HomeActionResult::None) - } - } - - fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> { - #[cfg(target_os = "android")] - { - return Self::begin_encrypted_secret_key_backup(password) - .map(|()| HomeActionResult::None); - } - - #[cfg(not(target_os = "android"))] - { - let _ = password; - Ok(HomeActionResult::None) - } - } - - fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> { - #[cfg(target_os = "android")] - { - return Self::poll_secret_key_export(); - } - - #[cfg(not(target_os = "android"))] - { - Ok(None) - } - } - - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "android")] - { - return self - .remote_signer - .take_update() - .transpose() - .map(|state| state.flatten()); - } - - #[cfg(not(target_os = "android"))] - { - Ok(None) - } - } -} - -#[cfg(any(target_os = "android", test))] -#[cfg_attr(not(target_os = "android"), allow(dead_code))] -impl AndroidBackend { - fn new() -> Self { - #[cfg(target_os = "android")] - let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::start(); - - #[cfg(not(target_os = "android"))] - let offline_geocoder = offline_geocoder::AndroidOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::unavailable( - radroots_app_core::RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - radroots_app_core::RadrootsOfflineGeocoderPlatform::Android, - "android offline geocoder initialization is only wired on android targets", - ), - ); - - Self { - country_lookup: country_lookup::AndroidCountryLookup::new(), - offline_geocoder, - #[cfg(target_os = "android")] - remote_signer: remote_signer::AndroidRemoteSigner::new(), - reverse_lookup: reverse_lookup::AndroidReverseLookup::new(), - } - } - - #[cfg(target_os = "android")] - fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { - #[cfg(target_os = "android")] - { - return storage::accounts_manager(); - } - } - - #[cfg(test)] - fn unsupported_identity_state() -> IdentityGateState { - IdentityGateState::Unsupported { - reason: ANDROID_SETUP_UNAVAILABLE_REASON.to_owned(), - } - } - - #[cfg(test)] - fn unsupported_setup_action_state() -> SetupActionState { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: false, - pending: false, - } - } - - fn enabled_setup_action_state() -> SetupActionState { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - } - } - - fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState { - match status { - RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }, - RadrootsNostrSelectedAccountStatus::NotConfigured - | RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, - } - } - - fn identity_state_from_manager( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) - } - - fn account_roster_from_manager( - manager: &RadrootsNostrAccountsManager, - ) -> Result<Vec<RadrootsAccountSummary>, String> { - manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| { - #[cfg(target_os = "android")] - let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?; - #[cfg(not(target_os = "android"))] - let custody = RadrootsAccountCustody::LocalManaged; - Ok(RadrootsAccountSummary { - account_id: record.account_id.to_string(), - npub: record.public_identity.public_key_npub, - label: record.label, - custody, - }) - }) - .collect() - } - - fn generate_local_identity( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - manager - .generate_identity(Some("local".to_owned()), true) - .map_err(|source| source.to_string())?; - Self::identity_state_from_manager(manager) - } - - fn export_selected_local_encrypted_secret_key( - manager: &RadrootsNostrAccountsManager, - password: &str, - ) -> Result<String, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - identity - .encrypt_secret_key_ncryptsec(password) - .map_err(|source| source.to_string()) - } - - fn export_selected_local_raw_secret_key( - manager: &RadrootsNostrAccountsManager, - ) -> Result<String, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - Ok(identity.nsec()) - } - - fn import_local_identity( - manager: &RadrootsNostrAccountsManager, - request: &RadrootsSecretImportRequest, - ) -> Result<IdentityGateState, String> { - let identity = match request.mode { - RadrootsSecretImportMode::EncryptedSecretKey => { - let Some(password) = request.password.as_deref() else { - return Err("password is required to import an encrypted secret key".to_owned()); - }; - RadrootsIdentity::from_encrypted_secret_key_str( - request.secret_text.as_str(), - password, - ) - .map_err(|_| "invalid encrypted secret key or password".to_owned())? - } - RadrootsSecretImportMode::RawSecretKey => { - RadrootsIdentity::from_secret_key_str(request.secret_text.as_str()) - .map_err(|_| "invalid raw secret key".to_owned())? - } - }; - - manager - .upsert_identity(&identity, None, true) - .map_err(|source| source.to_string())?; - - Self::identity_state_from_manager(manager) - } - - #[cfg(target_os = "android")] - fn begin_encrypted_secret_key_backup(password: &str) -> Result<(), String> { - *PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to store pending encrypted secret key backup".to_owned())? = - Some(PendingSecretKeyExport::EncryptedBackup { - password: Zeroizing::new(password.to_owned()), - }); - if let Err(source) = - android_security::begin_user_presence_verification("back up the current secret key") - { - *PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to clear pending encrypted secret key backup".to_owned())? = - None; - return Err(source.to_string()); - } - Ok(()) - } - - #[cfg(not(target_os = "android"))] - fn begin_encrypted_secret_key_backup(password: &str) -> Result<(), String> { - let _ = password; - Ok(()) - } - - #[cfg(target_os = "android")] - fn begin_raw_secret_key_reveal() -> Result<(), String> { - *PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to store pending raw secret key reveal".to_owned())? = - Some(PendingSecretKeyExport::RawReveal); - if let Err(source) = - android_security::begin_user_presence_verification("reveal the current secret key") - { - *PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to clear pending raw secret key reveal".to_owned())? = None; - return Err(source.to_string()); - } - Ok(()) - } - - #[cfg(not(target_os = "android"))] - fn begin_raw_secret_key_reveal() -> Result<(), String> { - Ok(()) - } - - #[cfg(target_os = "android")] - fn secret_key_export_pending() -> bool { - android_security::is_user_presence_verification_pending().unwrap_or(false) - } - - #[cfg(not(target_os = "android"))] - fn secret_key_export_pending() -> bool { - false - } - - #[cfg(target_os = "android")] - fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> { - match android_security::take_user_presence_verification_result() - .map_err(|source| source.to_string())? - { - Some(android_security::AndroidUserPresenceVerificationResult::Verified) => { - let manager = Self::accounts_manager()?; - let pending_export = PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to take pending secret key export".to_owned())? - .take(); - match pending_export { - Some(PendingSecretKeyExport::EncryptedBackup { password }) => { - Self::export_selected_local_encrypted_secret_key( - &manager, - password.as_str(), - ) - .map(|ncryptsec| { - Some(HomeActionResult::RevealEncryptedSecretKey { ncryptsec }) - }) - } - Some(PendingSecretKeyExport::RawReveal) => { - Self::export_selected_local_raw_secret_key(&manager) - .map(|nsec| Some(HomeActionResult::RevealRawSecretKey { nsec })) - } - None => Err("missing pending secret key export request".to_owned()), - } - } - Some(android_security::AndroidUserPresenceVerificationResult::Failed(message)) => { - *PENDING_SECRET_KEY_EXPORT - .lock() - .map_err(|_| "failed to clear pending secret key export".to_owned())? = None; - Err(message) - } - None => Ok(None), - } - } - - #[cfg(not(target_os = "android"))] - fn poll_secret_key_export() -> Result<Option<HomeActionResult>, String> { - Ok(None) - } - - fn remove_selected_local_identity( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(IdentityGateState::Missing); - }; - - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - Self::identity_state_from_manager(manager) - } - - fn remove_all_local_identities( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let account_ids = manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| record.account_id) - .collect::<Vec<_>>(); - - for account_id in account_ids { - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - } - - Self::identity_state_from_manager(manager) - } - - fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> { - match std::fs::remove_file(accounts_path) { - Ok(()) => Ok(()), - Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(source) => Err(format!("failed to remove android accounts file: {source}")), - } - } - - #[cfg(target_os = "android")] - fn reset_local_device_state( - manager: &RadrootsNostrAccountsManager, - accounts_path: &Path, - ) -> Result<IdentityGateState, String> { - remote_signer::purge_all_custody_state()?; - let state = Self::remove_all_local_identities(manager)?; - Self::remove_accounts_file_if_present(accounts_path)?; - Ok(state) - } -} - -#[cfg(any(target_os = "android", test))] -#[cfg(test)] -const ANDROID_SETUP_UNAVAILABLE_REASON: &str = "Secure onboarding is not yet available on Android."; - -#[cfg(target_os = "android")] -fn native_options(android_app: AndroidApp) -> eframe::NativeOptions { - eframe::NativeOptions { - renderer: eframe::Renderer::Glow, - android_app: Some(android_app), - viewport: ViewportBuilder::default().with_title(APP_NAME), - ..Default::default() - } -} - -#[cfg(target_os = "android")] -fn run_android_app(android_app: AndroidApp) -> Result<(), String> { - android_logger::init_once(Config::default().with_max_level(log::LevelFilter::Info)); - eframe::run_native( - APP_NAME, - native_options(android_app), - Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(AndroidBackend::new()))))), - ) - .map_err(|err| err.to_string()) -} - -#[cfg(target_os = "android")] -#[allow(improper_ctypes_definitions)] -#[allow(unsafe_code)] -#[unsafe(no_mangle)] -pub extern "C" fn android_main(android_app: AndroidApp) { - if let Err(err) = run_android_app(android_app) { - log::error!("android launcher failed: {err}"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{ - FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec, - }; - - #[test] - fn android_backend_reports_android_disabled_state_off_target() { - assert_eq!( - AndroidBackend::unsupported_identity_state(), - IdentityGateState::Unsupported { - reason: ANDROID_SETUP_UNAVAILABLE_REASON.to_owned(), - } - ); - assert_eq!( - AndroidBackend::unsupported_setup_action_state(), - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: false, - pending: false, - } - ); - } - - #[test] - fn android_backend_enables_setup_action_when_android_keygen_is_wired() { - assert_eq!( - AndroidBackend::enabled_setup_action_state(), - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - } - ); - } - - #[test] - fn android_backend_maps_ready_account_to_ready_state() { - let identity = RadrootsIdentity::generate(); - let account = - RadrootsNostrAccountRecord::new(identity.to_public(), Some("local".into()), 0); - - let state = AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::Ready { - account: account.clone(), - }); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: account.account_id.to_string(), - } - ); - } - - #[test] - fn android_backend_maps_fresh_and_public_only_accounts_to_missing() { - let public_only_identity = RadrootsIdentity::generate(); - let public_only_account = - RadrootsNostrAccountRecord::new(public_only_identity.to_public(), None, 0); - - assert_eq!( - AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::NotConfigured), - IdentityGateState::Missing - ); - assert_eq!( - AndroidBackend::map_status(RadrootsNostrSelectedAccountStatus::PublicOnly { - account: public_only_account, - }), - IdentityGateState::Missing - ); - } - - #[test] - fn fresh_android_manager_starts_in_setup_state() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - assert_eq!( - AndroidBackend::identity_state_from_manager(&manager), - Ok(IdentityGateState::Missing) - ); - } - - #[test] - fn local_identity_generation_transitions_android_to_ready() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - let state = AndroidBackend::generate_local_identity(&manager).expect("generate identity"); - let IdentityGateState::Ready { account_id } = state else { - panic!("expected ready identity state"); - }; - - assert!(!account_id.is_empty()); - } - - #[test] - fn local_identity_removal_transitions_android_back_to_missing() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - AndroidBackend::generate_local_identity(&manager).expect("generate identity"); - let state = AndroidBackend::remove_selected_local_identity(&manager) - .expect("remove selected account"); - - assert_eq!(state, IdentityGateState::Missing); - assert_eq!( - manager.selected_account_id().expect("selected account"), - None - ); - } - - #[test] - fn remove_all_local_identities_clears_every_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - manager - .generate_identity(Some("first".into()), true) - .expect("generate first"); - manager - .generate_identity(Some("second".into()), false) - .expect("generate second"); - - let state = AndroidBackend::remove_all_local_identities(&manager).expect("reset state"); - - assert_eq!(state, IdentityGateState::Missing); - assert_eq!(manager.list_accounts().expect("list accounts").len(), 0); - assert_eq!(manager.selected_account_id().expect("selected"), None); - } - - #[test] - fn export_selected_local_raw_secret_key_returns_nsec() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - manager - .upsert_identity(&identity, Some("primary".into()), true) - .expect("store identity"); - - let nsec = - AndroidBackend::export_selected_local_raw_secret_key(&manager).expect("export secret"); - - assert_eq!(nsec, identity.nsec()); - assert!(nsec.starts_with("nsec1")); - } - - #[test] - fn export_selected_local_encrypted_secret_key_returns_ncryptsec() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - - manager - .upsert_identity(&fixture_identity, Some("primary".into()), true) - .expect("store identity"); - - let ncryptsec = AndroidBackend::export_selected_local_encrypted_secret_key( - &manager, - FIXTURE_BACKUP_PASSWORD, - ) - .expect("export encrypted secret"); - - let restored = RadrootsIdentity::from_encrypted_secret_key_str( - ncryptsec.as_str(), - FIXTURE_BACKUP_PASSWORD, - ) - .expect("restore encrypted secret"); - - assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex); - } - - #[test] - fn import_local_identity_imports_raw_secret_key_and_selects_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - let state = AndroidBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::RawSecretKey, - secret_text: identity.nsec(), - password: None, - }, - ) - .expect("import"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: identity.id().to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(identity.id()) - ); - assert_eq!(manager.list_accounts().expect("list").len(), 1); - assert_eq!( - manager - .export_secret_hex(&identity.id()) - .expect("export secret"), - Some(identity.secret_key_hex()) - ); - } - - #[test] - fn import_local_identity_imports_encrypted_secret_key_and_selects_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - let fixture_account_id = fixture_identity.id(); - - let state = AndroidBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::EncryptedSecretKey, - secret_text: encrypted_secret_key, - password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()), - }, - ) - .expect("import"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: fixture_account_id.to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(fixture_account_id.clone()) - ); - assert_eq!(manager.list_accounts().expect("list").len(), 1); - assert_eq!( - manager - .export_secret_hex(&fixture_account_id) - .expect("export secret"), - Some(FIXTURE_ALICE.secret_key_hex.to_owned()) - ); - } - - #[test] - fn remove_accounts_file_if_present_deletes_existing_file() { - let unique = format!( - "radroots-android-reset-{}-{}.json", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time") - .as_nanos() - ); - let path = std::env::temp_dir().join(unique); - std::fs::write(&path, b"{}").expect("write accounts file"); - - AndroidBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file"); - - assert!(!path.exists()); - } -} diff --git a/crates/launchers/android/src/offline_geocoder.rs b/crates/launchers/android/src/offline_geocoder.rs @@ -1,600 +0,0 @@ -#![cfg_attr(not(target_os = "android"), allow(dead_code))] - -use radroots_app_core::{ - RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, -}; -#[cfg(any(target_os = "android", test))] -use radroots_geocoder::{ - Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, - GeocoderReverseResult, -}; -use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[cfg(target_os = "android")] -use jni::objects::{JClass, JObject, JString}; -#[cfg(target_os = "android")] -use jni::sys::jobject; -#[cfg(target_os = "android")] -use jni::{JNIEnv, JavaVM}; -#[cfg(target_os = "android")] -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError; - -#[cfg(target_os = "android")] -const ANDROID_APP_BRIDGE_CLASS: &str = "org.radroots.app.android.RadRootsAndroidAppBridge"; - -#[derive(Clone)] -pub(crate) struct AndroidOfflineGeocoder { - current: Arc<Mutex<RadrootsOfflineGeocoderState>>, - changed: Arc<AtomicBool>, -} - -impl AndroidOfflineGeocoder { - pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self { - Self { - current: Arc::new(Mutex::new(state)), - changed: Arc::new(AtomicBool::new(false)), - } - } - - #[cfg(target_os = "android")] - pub(crate) fn start() -> Self { - let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing); - let current = Arc::clone(&tracker.current); - let changed = Arc::clone(&tracker.changed); - - std::thread::spawn(move || { - let state = initialize_offline_geocoder(); - if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state { - log::warn!("android offline geocoder unavailable: {debug_message}"); - } - if let Ok(mut slot) = current.lock() { - *slot = state; - changed.store(true, Ordering::Release); - } - }); - - tracker - } - - pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState { - self.current - .lock() - .map(|state| state.clone()) - .unwrap_or_else(|_| { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Android, - "android offline geocoder state lock poisoned", - ) - }) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> { - if self.changed.swap(false, Ordering::AcqRel) { - Some(self.current_state()) - } else { - None - } - } -} - -#[cfg(any(target_os = "android", test))] -pub(crate) fn reverse_location( - state: &RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, -) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(state)?; - let options = options.map(|options| GeocoderReverseOptions { - limit: options.limit, - degree_offset: options.degree_offset, - }); - geocoder - .reverse( - GeocoderPoint { - lat: point.lat, - lng: point.lng, - }, - options, - ) - .map(|results| results.into_iter().map(map_reverse_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -#[cfg(any(target_os = "android", test))] -pub(crate) fn list_countries( - state: &RadrootsOfflineGeocoderState, -) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(state)?; - geocoder - .country_list() - .map(|results| results.into_iter().map(map_country_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -#[cfg(any(target_os = "android", test))] -pub(crate) fn country_center( - state: &RadrootsOfflineGeocoderState, - country_id: &str, -) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(state)?; - geocoder - .country_center(country_id) - .map(|point| RadrootsLocationPoint { - lat: point.lat, - lng: point.lng, - }) - .map_err(map_country_center_error) -} - -#[cfg(target_os = "android")] -fn initialize_offline_geocoder() -> RadrootsOfflineGeocoderState { - match initialize_offline_geocoder_inner() { - Ok(()) => RadrootsOfflineGeocoderState::Ready, - Err((kind, asset_revision, debug_message)) => match asset_revision { - Some(asset_revision) => RadrootsOfflineGeocoderState::unavailable_with_revision( - kind, - RadrootsOfflineGeocoderPlatform::Android, - asset_revision, - debug_message, - ), - None => RadrootsOfflineGeocoderState::unavailable( - kind, - RadrootsOfflineGeocoderPlatform::Android, - debug_message, - ), - }, - } -} - -#[cfg(target_os = "android")] -fn initialize_offline_geocoder_inner() -> Result< - (), - ( - RadrootsOfflineGeocoderUnavailableKind, - Option<String>, - String, - ), -> { - let staged_path = stage_offline_geocoder_asset() - .map_err(|(kind, debug_message)| (kind, None, debug_message))?; - let asset_revision = staged_asset_revision(staged_path.as_str()).map_err(|debug_message| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - None, - debug_message, - ) - })?; - Geocoder::open_path(staged_path.as_str()).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - Some(asset_revision.clone()), - format!("failed to open staged android geocoder db: {source}"), - ) - })?; - let _ = prune_stale_revisions(staged_path.as_str()); - Ok(()) -} - -#[cfg(any(target_os = "android", test))] -fn geocoder_for_queries( - state: &RadrootsOfflineGeocoderState, -) -> Result<Geocoder, RadrootsLocationResolverError> { - match state { - RadrootsOfflineGeocoderState::Initializing => { - return Err(RadrootsLocationResolverError::Initializing); - } - RadrootsOfflineGeocoderState::Unavailable { .. } => { - return Err(RadrootsLocationResolverError::Unavailable); - } - RadrootsOfflineGeocoderState::Ready => {} - } - - #[cfg(target_os = "android")] - { - let staged_path = stage_offline_geocoder_asset() - .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?; - Geocoder::open_path(staged_path.as_str()).map_err(|source| { - RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - } - }) - } - - #[cfg(not(target_os = "android"))] - { - Err(RadrootsLocationResolverError::QueryFailed { - message: "android location resolver queries are only available on android runtime" - .to_owned(), - }) - } -} - -#[cfg(target_os = "android")] -fn stage_offline_geocoder_asset() -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> -{ - let java_vm = android_java_vm().map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })?; - let mut env = java_vm - .attach_current_thread() - .map_err(jni_error) - .map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })?; - let bridge_class = bridge_class(&mut env).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })?; - let value = env - .call_static_method( - &bridge_class, - "stageOfflineGeocoderAsset", - "()Ljava/lang/String;", - &[], - ) - .and_then(|value| value.l()) - .map_err(jni_error) - .map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })?; - - if value.is_null() { - let error_kind = take_last_error_kind(&mut env, &bridge_class).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })?; - let debug_message = take_last_error_message(&mut env, &bridge_class) - .map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - source.to_string(), - ) - })? - .unwrap_or_else(|| "android app bridge returned no staged geocoder path".to_owned()); - return Err((error_kind, debug_message)); - } - - let value = JString::from(value); - env.get_string(&value) - .map(|value| value.into()) - .map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - jni_error(source).to_string(), - ) - }) -} - -#[cfg(target_os = "android")] -#[allow(unsafe_code)] -fn android_java_vm() -> Result<JavaVM, RadrootsNostrAccountsError> { - let context = ndk_context::android_context(); - // SAFETY: ndk_context is initialized by the Android runtime before this code runs and - // returns a stable JavaVM pointer for the current process. - unsafe { JavaVM::from_raw(context.vm().cast()) }.map_err(jni_error) -} - -#[cfg(target_os = "android")] -#[allow(unsafe_code)] -fn bridge_class<'local>( - env: &mut JNIEnv<'local>, -) -> Result<JClass<'local>, RadrootsNostrAccountsError> { - let context = ndk_context::android_context(); - // SAFETY: ndk_context stores a live process-wide Context jobject for the active Android app. - let context = unsafe { JObject::from_raw(context.context() as jobject) }; - let context = env.new_local_ref(&context).map_err(jni_error)?; - let class_loader = env - .call_method(&context, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]) - .and_then(|value| value.l()) - .map_err(jni_error)?; - let class_name = env - .new_string(ANDROID_APP_BRIDGE_CLASS) - .map_err(jni_error)?; - let class_name = JObject::from(class_name); - let bridge_class = env - .call_method( - &class_loader, - "loadClass", - "(Ljava/lang/String;)Ljava/lang/Class;", - &[jni::objects::JValue::Object(&class_name)], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - Ok(JClass::from(bridge_class)) -} - -#[cfg(target_os = "android")] -fn take_last_error_kind( - env: &mut JNIEnv<'_>, - bridge_class: &JClass<'_>, -) -> Result<RadrootsOfflineGeocoderUnavailableKind, RadrootsNostrAccountsError> { - let value = env - .call_static_method(bridge_class, "takeLastErrorKind", "()I", &[]) - .and_then(|value| value.i()) - .map_err(jni_error)?; - match value { - 1 => Ok(RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset), - 2 => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed), - 3 => Ok(RadrootsOfflineGeocoderUnavailableKind::InternalError), - _ => Ok(RadrootsOfflineGeocoderUnavailableKind::InitializationFailed), - } -} - -#[cfg(target_os = "android")] -fn take_last_error_message( - env: &mut JNIEnv<'_>, - bridge_class: &JClass<'_>, -) -> Result<Option<String>, RadrootsNostrAccountsError> { - let value = env - .call_static_method( - bridge_class, - "takeLastErrorMessage", - "()Ljava/lang/String;", - &[], - ) - .and_then(|value| value.l()) - .map_err(jni_error)?; - if value.is_null() { - return Ok(None); - } - let value = JString::from(value); - let value: String = env.get_string(&value).map_err(jni_error)?.into(); - Ok(Some(value)) -} - -#[cfg(target_os = "android")] -fn jni_error(error: jni::errors::Error) -> RadrootsNostrAccountsError { - RadrootsNostrAccountsError::Store(format!("android jni error: {error}")) -} - -fn prune_stale_revisions(staged_path: &str) -> Result<(), String> { - let staged_path = Path::new(staged_path); - let Some(active_revision_dir) = staged_path.parent() else { - return Err("android staged geocoder path did not have a revision directory".to_owned()); - }; - let Some(staged_root) = active_revision_dir.parent() else { - return Err( - "android staged geocoder path did not have a geocoder root directory".to_owned(), - ); - }; - let Some(active_revision) = active_revision_dir.file_name() else { - return Err("android staged geocoder revision directory did not have a name".to_owned()); - }; - - if !staged_root.is_dir() { - return Ok(()); - } - - for entry in std::fs::read_dir(staged_root) - .map_err(|source| format!("failed to list android geocoder revisions: {source}"))? - { - let entry = entry.map_err(|source| { - format!("failed to read android geocoder revision entry: {source}") - })?; - if entry.file_name() == active_revision { - continue; - } - - let path = entry.path(); - if entry - .file_type() - .map_err(|source| { - format!("failed to inspect android geocoder revision entry: {source}") - })? - .is_dir() - { - std::fs::remove_dir_all(path.as_path()).map_err(|source| { - format!( - "failed to remove stale android geocoder revision {}: {source}", - path.display() - ) - })?; - } else { - std::fs::remove_file(path.as_path()).map_err(|source| { - format!( - "failed to remove stale android geocoder revision file {}: {source}", - path.display() - ) - })?; - } - } - - Ok(()) -} - -fn staged_asset_revision(staged_path: &str) -> Result<String, String> { - let staged_path = Path::new(staged_path); - let Some(active_revision_dir) = staged_path.parent() else { - return Err("android staged geocoder path did not have a revision directory".to_owned()); - }; - let Some(active_revision) = active_revision_dir.file_name() else { - return Err("android staged geocoder revision directory did not have a name".to_owned()); - }; - let revision = active_revision.to_string_lossy(); - if revision.len() != 64 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { - return Err( - "android staged geocoder revision directory name was not a sha256 hex revision" - .to_owned(), - ); - } - Ok(revision.into_owned()) -} - -fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation { - RadrootsResolvedLocation { - id: result.id, - name: result.name, - admin1_id: result.admin1_id, - admin1_name: result.admin1_name, - country_id: result.country_id, - country_name: result.country_name, - point: RadrootsLocationPoint { - lat: result.latitude, - lng: result.longitude, - }, - } -} - -fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry { - RadrootsLocationCountry { - country_id: result.country_id, - country_name: result.country, - center: RadrootsLocationPoint { - lat: result.lat, - lng: result.lng, - }, - } -} - -fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError { - match source { - GeocoderError::CountryCenterNotFound { country_id } => { - RadrootsLocationResolverError::CountryCenterNotFound { country_id } - } - other => RadrootsLocationResolverError::QueryFailed { - message: other.to_string(), - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[test] - fn missing_asset_maps_to_build_unavailable_message() { - let state = RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Android, - "android bundled geocoder asset missing at assets/geocoder/geonames.db", - ); - - assert_eq!( - state, - RadrootsOfflineGeocoderState::Unavailable { - kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - platform: RadrootsOfflineGeocoderPlatform::Android, - asset_revision: None, - debug_message: - "android bundled geocoder asset missing at assets/geocoder/geonames.db" - .to_owned(), - } - ); - } - - #[test] - fn staged_asset_revision_reads_sha256_directory_name() { - let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"; - let staged_path = format!("/tmp/radroots/android/geocoder/{revision}/geonames.db"); - - assert_eq!( - staged_asset_revision(staged_path.as_str()).unwrap(), - revision - ); - } - - #[test] - fn prune_stale_revisions_keeps_active_revision_only() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-android-geocoder-prune-test-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let staged_root = temp_root.join("geocoder"); - let active_dir = staged_root.join("active"); - let stale_dir = staged_root.join("stale"); - let stale_file = staged_root.join("orphan.txt"); - let staged_path = active_dir.join("geonames.db"); - - std::fs::create_dir_all(active_dir.as_path()).unwrap(); - std::fs::create_dir_all(stale_dir.as_path()).unwrap(); - std::fs::write(staged_path.as_path(), b"active").unwrap(); - std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap(); - std::fs::write(stale_file.as_path(), b"orphan").unwrap(); - - prune_stale_revisions(staged_path.to_str().unwrap()).unwrap(); - - assert!(active_dir.exists()); - assert!(!stale_dir.exists()); - assert!(!stale_file.exists()); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn reverse_result_mapping_preserves_location_fields() { - let resolved = map_reverse_result(GeocoderReverseResult { - id: 123, - name: "Lusaka".to_owned(), - admin1_id: Some(456), - admin1_name: Some("Lusaka".to_owned()), - country_id: "ZM".to_owned(), - country_name: Some("Zambia".to_owned()), - latitude: -15.4167, - longitude: 28.2833, - }); - - assert_eq!( - resolved, - RadrootsResolvedLocation { - id: 123, - name: "Lusaka".to_owned(), - admin1_id: Some(456), - admin1_name: Some("Lusaka".to_owned()), - country_id: "ZM".to_owned(), - country_name: Some("Zambia".to_owned()), - point: RadrootsLocationPoint { - lat: -15.4167, - lng: 28.2833, - }, - } - ); - } - - #[test] - fn unavailable_state_blocks_queries_until_ready() { - let state = RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Android, - "missing android geocoder asset", - ); - - assert_eq!( - reverse_location(&state, RadrootsLocationPoint { lat: 0.0, lng: 0.0 }, None,), - Err(RadrootsLocationResolverError::Unavailable) - ); - assert_eq!( - list_countries(&state), - Err(RadrootsLocationResolverError::Unavailable) - ); - assert_eq!( - country_center(&state, "US"), - Err(RadrootsLocationResolverError::Unavailable) - ); - } -} diff --git a/crates/launchers/android/src/remote_signer.rs b/crates/launchers/android/src/remote_signer.rs @@ -1,471 +0,0 @@ -use crate::storage; -use radroots_app_android_security::{ANDROID_NOSTR_SERVICE, RadrootsAndroidKeystoreVault}; -use radroots_app_core::{ - IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection, - RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState, -}; -use radroots_app_remote_signer::{ - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController, - RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState, - RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, - RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session, - radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, - radroots_app_remote_signer_purge_all_custody_state, - radroots_app_remote_signer_reconcile_startup, -}; -use radroots_identity::RadrootsIdentityId; -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault, - account_secret_slot, -}; -use std::path::{Path, PathBuf}; - -const REMOTE_SIGNER_LABEL: &str = "remote signer"; - -#[derive(Clone, Copy)] -struct AndroidRemoteSignerHooks; - -impl RadrootsAppRemoteSignerControllerHooks for AndroidRemoteSignerHooks { - type ReadyState = IdentityGateState; - - fn reconcile_startup_state(&self) -> Result<(), String> { - let manager = crate::storage::accounts_manager()?; - let store_path = sessions_path()?; - radroots_app_remote_signer_reconcile_startup( - &manager, - store_path.as_path(), - REMOTE_SIGNER_LABEL, - load_client_secret, - remove_client_secret, - purge_client_secret_namespace, - ) - } - - fn store_pending_session( - &self, - pending: &RadrootsAppRemoteSignerPendingSession, - ) -> Result<(), String> { - let client_account_id = pending.record.client_account_id().to_owned(); - store_client_secret( - client_account_id.as_str(), - pending.client_secret_key_hex.as_str(), - )?; - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - if let Err(error) = state.upsert_pending(pending.record.clone()) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error.to_string()); - } - if let Err(error) = save_sessions(store_path.as_path(), &state) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error); - } - Ok(()) - } - - fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - pending_session_record() - } - - fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> { - load_client_secret(client_account_id) - } - - fn activate_pending_session( - &self, - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, - ) -> Result<Self::ReadyState, String> { - activate_remote_session(client_account_id, approved) - } - - fn clear_pending_session( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret) - } -} - -#[derive(Clone)] -pub(crate) struct AndroidRemoteSigner { - controller: RadrootsAppRemoteSignerController<AndroidRemoteSignerHooks>, - action_controller: RadrootsAppRemoteSignerActionController<AndroidRemoteSignerHooks>, -} - -impl AndroidRemoteSigner { - pub(crate) fn new() -> Self { - Self { - controller: RadrootsAppRemoteSignerController::new(AndroidRemoteSignerHooks), - action_controller: RadrootsAppRemoteSignerActionController::new( - AndroidRemoteSignerHooks, - ), - } - } - - pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> { - self.controller.take_update() - } - - pub(crate) fn is_connecting(&self) -> bool { - self.controller.is_connecting() - } - - pub(crate) fn action_state(&self) -> Result<SetupActionState, String> { - if self.is_connecting() { - return Ok(SetupActionState { - label: "Connecting Remote Signer...".to_owned(), - enabled: false, - pending: true, - }); - } - - if self.pending_connection()?.is_some() { - return Ok(match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { - label: "Remote Signer Approval Check Retrying".to_owned(), - enabled: false, - pending: false, - }, - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => { - SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - } - } - RadrootsAppRemoteSignerPendingState::Idle - | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, - }, - }); - } - - Ok(SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: true, - pending: false, - }) - } - - pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> { - self.controller.begin_connect(input) - } - - pub(crate) fn pending_connection( - &self, - ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> { - Ok( - pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection { - signer_npub: record.signer_identity.public_key_npub, - relays: record.relays, - auth_url: match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url), - _ => None, - }, - }), - ) - } - - pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> { - if selected_remote_signer_account()?.is_none() { - return Ok(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }); - } - - Ok(match self.action_controller.state() { - RadrootsAppRemoteSignerActionState::Idle => SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: true, - pending: false, - }, - RadrootsAppRemoteSignerActionState::Signing => SetupActionState { - label: "Signing Remote Kind 1 Note...".to_owned(), - enabled: false, - pending: true, - }, - RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - }, - }) - } - - pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> { - self.action_controller.begin_sign_kind1_note(content) - } - - pub(crate) fn take_note_update( - &self, - ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> { - self.action_controller.take_update() - } -} - -pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { - let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; - let requested_permissions = preview.requested_permission_labels(); - Ok(RadrootsRemoteSignerPreview { - source_label: preview.source_label().to_owned(), - signer_npub: preview.signer_identity.public_key_npub, - relays: preview.relays, - requested_permissions, - }) -} - -pub(crate) fn identity_state_from_status( - status: RadrootsNostrSelectedAccountStatus, -) -> Result<IdentityGateState, String> { - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing), - RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if active_session_for_account_id(account.account_id.as_str())?.is_some() { - Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }) - } else { - Ok(IdentityGateState::Missing) - } - } - } -} - -pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> { - if active_session_for_account_id(account_id)?.is_some() { - Ok(RadrootsAccountCustody::RemoteSigner) - } else { - Ok(RadrootsAccountCustody::LocalManaged) - } -} - -pub(crate) fn disconnect_selected_remote_signer( - manager: &RadrootsNostrAccountsManager, -) -> Result<IdentityGateState, String> { - let store_path = sessions_path()?; - let status = radroots_app_remote_signer_disconnect_selected( - manager, - store_path.as_path(), - remove_client_secret, - )?; - identity_state_from_status(status) -} - -pub(crate) fn cancel_pending_connection() -> Result<(), String> { - let store_path = sessions_path()?; - let _ = radroots_app_remote_signer_clear_pending_session( - store_path.as_path(), - remove_client_secret, - )?; - Ok(()) -} - -pub(crate) fn purge_all_custody_state() -> Result<(), String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_purge_all_custody_state( - store_path.as_path(), - remove_client_secret, - purge_client_secret_namespace, - ) -} - -fn activate_remote_session( - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, -) -> Result<IdentityGateState, String> { - let manager = crate::storage::accounts_manager()?; - manager - .upsert_public_identity( - approved.user_identity.clone(), - Some(REMOTE_SIGNER_LABEL.to_owned()), - true, - ) - .map_err(|source| source.to_string())?; - let store_path = sessions_path()?; - let activation_result = (|| -> Result<(), String> { - let mut state = load_sessions(store_path.as_path())?; - state - .activate_session( - client_account_id, - approved.user_identity.clone(), - approved.relays.clone(), - approved.approved_permissions.clone(), - ) - .ok_or_else(|| { - "pending remote signer session disappeared before activation".to_owned() - })?; - save_sessions(store_path.as_path(), &state) - })(); - if let Err(error) = activation_result { - if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) { - return Err(format!( - "{error}. remote signer account rollback needs retry: {rollback_error}" - )); - } - return Err(error); - } - Ok(IdentityGateState::Ready { - account_id: approved.user_identity.id.to_string(), - }) -} - -fn selected_remote_signer_account() -> Result<Option<String>, String> { - let manager = crate::storage::accounts_manager()?; - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(None); - }; - if active_session_for_account_id(account_id.as_str())?.is_some() { - Ok(Some(account_id.to_string())) - } else { - Ok(None) - } -} - -fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> { - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else { - return Err("active remote signer session disappeared before relay update".to_owned()); - }; - if session.relays == relays { - return Ok(()); - } - session.relays = relays; - state.remove_active_session_for_account_id(account_id); - state.sessions.push(session); - save_sessions(store_path.as_path(), &state) -} - -impl RadrootsAppRemoteSignerActionControllerHooks for AndroidRemoteSignerHooks { - type ReadyState = RadrootsRemoteSignerSignedNote; - - fn selected_active_session( - &self, - ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Ok(None); - }; - let Some(record) = active_session_for_account_id(account_id.as_str())? else { - return Ok(None); - }; - let secret = load_client_secret(record.client_account_id())?; - Ok(Some((record, secret))) - } - - fn complete_sign_event( - &self, - signed_event: RadrootsAppRemoteSignerSignedEvent, - ) -> Result<Self::ReadyState, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Err("remote signer account is no longer selected".to_owned()); - }; - update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?; - Ok(RadrootsRemoteSignerSignedNote { - event_id_hex: signed_event.event_id_hex, - }) - } -} - -fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.pending_session().cloned()) -} - -fn active_session_for_account_id( - account_id: &str, -) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.active_session_for_account_id(account_id).cloned()) -} - -fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> { - RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string()) -} - -fn save_sessions( - path: &Path, - state: &RadrootsAppRemoteSignerSessionStoreState, -) -> Result<(), String> { - state.save(path).map_err(|error| error.to_string()) -} - -fn sessions_path() -> Result<PathBuf, String> { - Ok(storage::app_data_root()? - .join("nostr") - .join("remote-signer-sessions.json")) -} - -fn client_secret_vault() -> RadrootsAndroidKeystoreVault { - RadrootsAndroidKeystoreVault::new_with_namespace( - ANDROID_NOSTR_SERVICE, - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, - ) -} - -fn legacy_client_secret_vault() -> RadrootsAndroidKeystoreVault { - RadrootsAndroidKeystoreVault::new(ANDROID_NOSTR_SERVICE) -} - -fn client_secret_slot(client_account_id: &str) -> Result<String, String> { - let account_id = RadrootsIdentityId::try_from(client_account_id) - .map_err(|_| "invalid remote signer client account id".to_owned())?; - Ok(account_secret_slot(&account_id)) -} - -fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .store_secret(slot.as_str(), secret_key_hex) - .map_err(|source| source.to_string()) -} - -fn load_client_secret(client_account_id: &str) -> Result<String, String> { - let slot = client_secret_slot(client_account_id)?; - if let Some(secret) = client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - { - return Ok(secret); - } - - let secret = legacy_client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - .ok_or_else(|| "remote signer session secret is missing".to_owned())?; - let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str()); - let _ = legacy_client_secret_vault().remove_secret(slot.as_str()); - Ok(secret) -} - -fn remove_client_secret(client_account_id: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string())?; - legacy_client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string()) -} - -fn purge_client_secret_namespace() -> Result<(), String> { - client_secret_vault() - .purge_namespace() - .map_err(|source| source.to_string()) -} diff --git a/crates/launchers/android/src/reverse_lookup.rs b/crates/launchers/android/src/reverse_lookup.rs @@ -1,113 +0,0 @@ -#![cfg_attr(not(target_os = "android"), allow(dead_code))] - -#[cfg(target_os = "android")] -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult, -}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct AndroidReverseLookup { - result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>, - changed: Arc<AtomicBool>, - pending: Arc<AtomicBool>, -} - -impl AndroidReverseLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "android")] - pub(crate) fn begin( - &self, - geocoder_state: RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - if self.pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline location query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.result); - let changed = Arc::clone(&self.changed); - let pending = Arc::clone(&self.pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::reverse_location(&geocoder_state, point, options); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "android"))] - pub(crate) fn begin( - &self, - _geocoder_state: RadrootsOfflineGeocoderState, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> { - if !self.changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "android reverse lookup result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::RadrootsResolvedLocation; - - fn sample_result() -> RadrootsReverseLocationLookupResult { - Ok(vec![RadrootsResolvedLocation { - id: 7, - name: "example".to_owned(), - admin1_id: None, - admin1_name: None, - country_id: "US".to_owned(), - country_name: Some("United States".to_owned()), - point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 }, - }]) - } - - #[test] - fn take_update_is_none_until_tracker_changes() { - let tracker = AndroidReverseLookup::new(); - - assert_eq!(tracker.take_update(), None); - } - - #[test] - fn take_update_returns_queued_result_once() { - let tracker = AndroidReverseLookup::new(); - *tracker.result.lock().unwrap() = Some(sample_result()); - tracker.changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_update(), None); - } -} diff --git a/crates/launchers/android/src/storage.rs b/crates/launchers/android/src/storage.rs @@ -1,107 +0,0 @@ -#[cfg(target_os = "android")] -use radroots_app_android_security::{ - ANDROID_NOSTR_SERVICE, RadrootsAndroidKeystoreVault, resolve_radroots_base_root, -}; -use radroots_app_core::mobile_native_app_storage_layout; -#[cfg(target_os = "android")] -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; -use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform}; -use std::path::{Path, PathBuf}; - -fn app_paths_from_base_root(base_root: &Path) -> Result<RadrootsPaths, String> { - Ok(mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root)?.app_paths) -} - -#[cfg(target_os = "android")] -pub(crate) fn app_data_root() -> Result<PathBuf, String> { - let base_root = resolve_radroots_base_root().map_err(|source| source.to_string())?; - let root = app_data_root_from_base_root(base_root.as_path())?; - ensure_directory_tree(root.as_path())?; - Ok(root) -} - -#[cfg(target_os = "android")] -pub(crate) fn accounts_path() -> Result<PathBuf, String> { - let base_root = resolve_radroots_base_root().map_err(|source| source.to_string())?; - let accounts_path = accounts_path_from_base_root(base_root.as_path())?; - if let Some(parent) = accounts_path.parent() { - ensure_directory_tree(parent)?; - } - Ok(accounts_path) -} - -#[cfg(target_os = "android")] -pub(crate) fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { - RadrootsNostrAccountsManager::new_file_backed_with_vault( - accounts_path()?, - RadrootsAndroidKeystoreVault::new(ANDROID_NOSTR_SERVICE), - ) - .map_err(|source| source.to_string()) -} - -pub(crate) fn app_data_root_from_base_root(base_root: &Path) -> Result<PathBuf, String> { - Ok(app_paths_from_base_root(base_root)?.data) -} - -pub(crate) fn accounts_path_from_base_root(base_root: &Path) -> Result<PathBuf, String> { - Ok(app_data_root_from_base_root(base_root)? - .join("nostr") - .join("accounts.json")) -} - -#[cfg(target_os = "android")] -fn ensure_directory_tree(path: &Path) -> Result<(), String> { - std::fs::create_dir_all(path) - .map_err(|source| format!("failed to create android app data directory: {source}"))?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn accounts_path_uses_android_mobile_native_layout() { - let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots"); - - assert_eq!( - accounts_path_from_base_root(base_root.as_path()).expect("accounts path"), - PathBuf::from( - "/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr/accounts.json" - ) - ); - } - - #[test] - fn app_data_root_uses_android_mobile_native_layout() { - let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots"); - - assert_eq!( - app_data_root_from_base_root(base_root.as_path()).expect("app data root"), - PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app") - ); - } - - #[test] - fn mobile_paths_follow_shared_logical_root_model() { - let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots"); - let paths = app_paths_from_base_root(base_root.as_path()).expect("mobile paths"); - - assert_eq!( - paths.config, - PathBuf::from( - "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app" - ) - ); - assert_eq!( - paths.data, - PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app") - ); - assert_eq!( - paths.secrets, - PathBuf::from( - "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app" - ) - ); - } -} diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml @@ -1,44 +0,0 @@ -[package] -name = "radroots_app_desktop" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots desktop launcher" -publish = false -build = "build.rs" - -[lints] -workspace = true - -[dependencies] -eframe = { workspace = true, features = ["wgpu", "wayland", "x11"] } -egui.workspace = true -image.workspace = true -log.workspace = true -radroots_app_core = { path = "../../shared/core" } -radroots_app_remote_signer = { path = "../../shared/remote_signer" } -radroots_geocoder.workspace = true -radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] } -radroots_runtime_paths.workspace = true -zeroize.workspace = true - -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -wgpu = { workspace = true, features = ["metal", "wgsl"] } - -[target.'cfg(target_os = "macos")'.dependencies] -objc2-foundation = { workspace = true, features = ["NSProcessInfo", "NSString"] } -radroots_app_apple_security.workspace = true -radroots_identity.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -wgpu = { workspace = true, features = ["dx12", "wgsl"] } - -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] -wgpu = { workspace = true, features = ["vulkan", "gles", "wgsl"] } - -[dev-dependencies] -radroots_app_test_support = { path = "../../shared/test_support" } diff --git a/crates/launchers/desktop/assets/icons/radroots-logo.ico b/crates/launchers/desktop/assets/icons/radroots-logo.ico Binary files differ. diff --git a/crates/launchers/desktop/build.rs b/crates/launchers/desktop/build.rs @@ -1,214 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -const GEOCODER_DB_FILENAME: &str = "geonames.db"; -const GEOCODER_REVISION_FILENAME: &str = "geonames.revision"; - -fn main() { - println!("cargo:rerun-if-changed=build.rs"); - sync_optional_geocoder_assets(); - - if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() != Some("macos") { - return; - } - - let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir")); - let package_dir = - manifest_dir.join("../../../native/bridges/apple/security/swift/RadRootsAppleSecurity"); - let info_plist_path = manifest_dir.join("macos/Info.plist"); - - emit_rerun_paths(&package_dir); - println!("cargo:rerun-if-changed={}", info_plist_path.display()); - - let configuration = if env::var("PROFILE").ok().as_deref() == Some("release") { - "release" - } else { - "debug" - }; - let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target arch"); - - run_swift_build(&package_dir, configuration, &arch); - let bin_path = swift_bin_path(&package_dir, configuration, &arch); - - let dylib_path = bin_path.join("libRadRootsAppleSecurityFFIDynamic.dylib"); - if !dylib_path.is_file() { - panic!( - "swift package did not produce expected dynamic library at {}", - dylib_path.display() - ); - } - - let copied_library_dir = target_profile_dir(); - fs::copy( - &dylib_path, - copied_library_dir.join("libRadRootsAppleSecurityFFIDynamic.dylib"), - ) - .unwrap_or_else(|err| { - panic!( - "failed to copy swift ffi library from {} into {}: {err}", - dylib_path.display(), - copied_library_dir.display() - ) - }); - - println!( - "cargo:rustc-link-search=native={}", - copied_library_dir.display() - ); - println!("cargo:rustc-link-lib=dylib=RadRootsAppleSecurityFFIDynamic"); - println!("cargo:rustc-link-lib=framework=Foundation"); - println!("cargo:rustc-link-lib=framework=Security"); - println!("cargo:rustc-link-lib=framework=LocalAuthentication"); - println!( - "cargo:rustc-link-arg=-Wl,-rpath,{}", - copied_library_dir.display() - ); - println!( - "cargo:rustc-link-arg-bin=radroots_app_desktop=-Wl,-sectcreate,__TEXT,__info_plist,{}", - info_plist_path.display() - ); -} - -fn sync_optional_geocoder_assets() { - let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir")); - let source_db_path = - manifest_dir.join(format!("../../../assets/geocoder/{GEOCODER_DB_FILENAME}")); - let source_revision_path = manifest_dir.join(format!( - "../../../assets/geocoder/{GEOCODER_REVISION_FILENAME}" - )); - println!("cargo:rerun-if-changed={}", source_db_path.display()); - println!("cargo:rerun-if-changed={}", source_revision_path.display()); - - let profile_dir = target_profile_dir(); - let target_db_path = profile_dir.join(GEOCODER_DB_FILENAME); - let target_revision_path = profile_dir.join(GEOCODER_REVISION_FILENAME); - - if source_db_path.is_file() { - if !source_revision_path.is_file() { - panic!( - "stamped desktop geocoder revision asset missing at {}", - source_revision_path.display() - ); - } - - std::fs::copy(&source_db_path, &target_db_path).unwrap_or_else(|err| { - panic!( - "failed to copy optional desktop geocoder asset from {} to {}: {err}", - source_db_path.display(), - target_db_path.display() - ) - }); - std::fs::copy(&source_revision_path, &target_revision_path).unwrap_or_else(|err| { - panic!( - "failed to copy optional desktop geocoder revision from {} to {}: {err}", - source_revision_path.display(), - target_revision_path.display() - ) - }); - return; - } - - if target_db_path.exists() { - std::fs::remove_file(&target_db_path).unwrap_or_else(|err| { - panic!( - "failed to remove stale desktop geocoder asset at {}: {err}", - target_db_path.display() - ) - }); - } - if target_revision_path.exists() { - std::fs::remove_file(&target_revision_path).unwrap_or_else(|err| { - panic!( - "failed to remove stale desktop geocoder revision at {}: {err}", - target_revision_path.display() - ) - }); - } -} - -fn target_profile_dir() -> PathBuf { - let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); - out_dir - .ancestors() - .nth(3) - .unwrap_or_else(|| panic!("unexpected cargo OUT_DIR layout: {}", out_dir.display())) - .to_path_buf() -} - -fn emit_rerun_paths(package_dir: &Path) { - println!( - "cargo:rerun-if-changed={}", - package_dir.join("Package.swift").display() - ); - emit_rerun_dir(&package_dir.join("Sources")); -} - -fn emit_rerun_dir(dir: &Path) { - if !dir.is_dir() { - return; - } - - let mut entries = std::fs::read_dir(dir) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", dir.display())) - .map(|entry| entry.unwrap().path()) - .collect::<Vec<_>>(); - entries.sort(); - - for path in entries { - if path.is_dir() { - emit_rerun_dir(&path); - } else { - println!("cargo:rerun-if-changed={}", path.display()); - } - } -} - -fn run_swift_build(package_dir: &Path, configuration: &str, arch: &str) { - let status = Command::new("swift") - .arg("build") - .arg("--package-path") - .arg(package_dir) - .arg("--product") - .arg("RadRootsAppleSecurityFFIDynamic") - .arg("--configuration") - .arg(configuration) - .arg("--arch") - .arg(arch) - .status() - .unwrap_or_else(|err| panic!("failed to run swift build: {err}")); - - if !status.success() { - panic!("swift build failed for RadRootsAppleSecurityFFIDynamic"); - } -} - -fn swift_bin_path(package_dir: &Path, configuration: &str, arch: &str) -> PathBuf { - let output = Command::new("swift") - .arg("build") - .arg("--package-path") - .arg(package_dir) - .arg("--product") - .arg("RadRootsAppleSecurityFFIDynamic") - .arg("--configuration") - .arg(configuration) - .arg("--arch") - .arg(arch) - .arg("--show-bin-path") - .output() - .unwrap_or_else(|err| panic!("failed to resolve swift bin path: {err}")); - - if !output.status.success() { - panic!( - "swift build --show-bin-path failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - PathBuf::from( - String::from_utf8(output.stdout) - .expect("swift bin path utf-8") - .trim(), - ) -} diff --git a/crates/launchers/desktop/macos/Info.plist b/crates/launchers/desktop/macos/Info.plist @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>CFBundleIdentifier</key> - <string>org.radroots.app.desktop</string> - <key>CFBundleName</key> - <string>Rad Roots</string> - <key>CFBundleDisplayName</key> - <string>Rad Roots</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> -</dict> -</plist> diff --git a/crates/launchers/desktop/src/country_lookup.rs b/crates/launchers/desktop/src/country_lookup.rs @@ -1,187 +0,0 @@ -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationResolverError, RadrootsOfflineGeocoderState, -}; -#[cfg(target_os = "macos")] -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct DesktopCountryLookup { - country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>, - country_list_changed: Arc<AtomicBool>, - country_list_pending: Arc<AtomicBool>, - country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>, - country_center_changed: Arc<AtomicBool>, - country_center_pending: Arc<AtomicBool>, -} - -impl DesktopCountryLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "macos")] - pub(crate) fn begin_list( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_list_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country list query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_list_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_list_result); - let changed = Arc::clone(&self.country_list_changed); - let pending = Arc::clone(&self.country_list_pending); - std::thread::spawn(move || { - let lookup_result = - offline_geocoder::list_countries(app_data_root.as_path(), &geocoder_state); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "macos"))] - pub(crate) fn begin_list( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - #[cfg(target_os = "macos")] - pub(crate) fn begin_center( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_center_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country center query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_center_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_center_result); - let changed = Arc::clone(&self.country_center_changed); - let pending = Arc::clone(&self.country_center_pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::country_center( - app_data_root.as_path(), - &geocoder_state, - &country_id, - ); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "macos"))] - pub(crate) fn begin_center( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - _country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> { - if !self.country_list_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_list_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "desktop country list result lock poisoned".to_owned(), - })), - } - } - - pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> { - if !self.country_center_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_center_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "desktop country center result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint}; - - fn sample_countries() -> RadrootsLocationCountryListResult { - Ok(vec![RadrootsLocationCountry { - country_id: "BR".to_owned(), - country_name: Some("Brazil".to_owned()), - center: RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - }, - }]) - } - - #[test] - fn take_list_update_is_none_until_tracker_changes() { - let tracker = DesktopCountryLookup::new(); - - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_list_update_returns_queued_result_once() { - let tracker = DesktopCountryLookup::new(); - *tracker.country_list_result.lock().unwrap() = Some(sample_countries()); - tracker.country_list_changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_center_update_returns_queued_result_once() { - let tracker = DesktopCountryLookup::new(); - *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - })); - tracker - .country_center_changed - .store(true, Ordering::Release); - - assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235)); - assert_eq!(tracker.take_center_update(), None); - } -} diff --git a/crates/launchers/desktop/src/main.rs b/crates/launchers/desktop/src/main.rs @@ -1,1279 +0,0 @@ -#![forbid(unsafe_code)] -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use eframe::egui; -use image::ImageFormat; -#[cfg(all(target_os = "macos", not(test)))] -use radroots_app_apple_security::verify_user_presence; -#[cfg(target_os = "macos")] -use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault}; -use radroots_app_core::{ - APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, - ImportActionState, RadrootsAccountCustody, RadrootsAccountSummary, RadrootsApp, - RadrootsAppBackend, RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult, - RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, - RadrootsReverseLocationLookupResult, RadrootsSecretImportMode, RadrootsSecretImportRequest, - SetupActionState, interactive_user_app_storage_layout_with_resolver, -}; -#[cfg(target_os = "macos")] -use radroots_identity::RadrootsIdentity; -#[cfg(target_os = "macos")] -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, -}; -use radroots_runtime_paths::{RadrootsPathResolver, RadrootsPaths}; -use std::path::{Path, PathBuf}; -#[cfg(target_os = "macos")] -use zeroize::Zeroizing; - -mod country_lookup; -mod offline_geocoder; -#[cfg(target_os = "macos")] -mod remote_signer; -mod reverse_lookup; - -use country_lookup::DesktopCountryLookup; -use offline_geocoder::DesktopOfflineGeocoder; -#[cfg(target_os = "macos")] -use remote_signer::DesktopRemoteSigner; -use reverse_lookup::DesktopReverseLookup; - -const RADROOTS_DESKTOP_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/radroots-logo.ico"); - -#[cfg(target_os = "macos")] -fn set_macos_app_name() { - use objc2_foundation::{NSProcessInfo, NSString}; - - let process_info = NSProcessInfo::processInfo(); - let process_name = NSString::from_str(APP_NAME); - process_info.setProcessName(&process_name); -} - -#[cfg(not(target_os = "macos"))] -fn set_macos_app_name() {} - -fn desktop_icon() -> Option<egui::IconData> { - let image = - image::load_from_memory_with_format(RADROOTS_DESKTOP_ICON_BYTES, ImageFormat::Ico).ok()?; - let image = image.into_rgba8(); - let (width, height) = image.dimensions(); - Some(egui::IconData { - rgba: image.into_raw(), - width, - height, - }) -} - -struct DesktopBackend { - country_lookup: DesktopCountryLookup, - offline_geocoder: DesktopOfflineGeocoder, - #[cfg(target_os = "macos")] - remote_signer: DesktopRemoteSigner, - reverse_lookup: DesktopReverseLookup, -} - -impl DesktopBackend { - fn app_paths_with_resolver(resolver: &RadrootsPathResolver) -> Result<RadrootsPaths, String> { - Ok(interactive_user_app_storage_layout_with_resolver(resolver)?.app_paths) - } - - fn new() -> Self { - #[cfg(target_os = "macos")] - let offline_geocoder = match Self::app_data_root() { - Ok(app_data_root) => DesktopOfflineGeocoder::start(app_data_root), - Err(debug_message) => { - DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Desktop, - debug_message, - )) - } - }; - - #[cfg(not(target_os = "macos"))] - let offline_geocoder = - DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Desktop, - "desktop offline geocoder initialization is only wired for macos", - )); - - Self { - country_lookup: DesktopCountryLookup::new(), - offline_geocoder, - #[cfg(target_os = "macos")] - remote_signer: DesktopRemoteSigner::new(), - reverse_lookup: DesktopReverseLookup::new(), - } - } - - fn radroots_root() -> Result<PathBuf, String> { - Ok( - interactive_user_app_storage_layout_with_resolver(&RadrootsPathResolver::current())? - .runtime_root, - ) - } - - fn app_data_root() -> Result<PathBuf, String> { - Ok(Self::app_paths_with_resolver(&RadrootsPathResolver::current())?.data) - } - - fn private_directory_chain(root: &Path, leaf: &Path) -> Result<Vec<PathBuf>, String> { - let relative = leaf - .strip_prefix(root) - .map_err(|_| "private directory escaped radroots root".to_owned())?; - let mut current = root.to_path_buf(); - let mut chain = vec![current.clone()]; - for component in relative.components() { - current.push(component); - chain.push(current.clone()); - } - Ok(chain) - } - - #[cfg(target_os = "macos")] - fn ensure_private_directory_tree(leaf: &Path) -> Result<(), String> { - use std::os::unix::fs::PermissionsExt; - - std::fs::create_dir_all(leaf) - .map_err(|source| format!("failed to create accounts directory: {source}"))?; - - for path in Self::private_directory_chain(&Self::radroots_root()?, leaf)? { - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).map_err( - |source| format!("failed to set private directory permissions: {source}"), - )?; - } - - Ok(()) - } - - fn accounts_path() -> Result<PathBuf, String> { - Ok(Self::app_data_root()?.join("nostr").join("accounts.json")) - } - - #[cfg(target_os = "macos")] - fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { - let accounts_path = Self::accounts_path()?; - if let Some(parent) = accounts_path.parent() { - Self::ensure_private_directory_tree(parent)?; - } - - RadrootsNostrAccountsManager::new_file_backed_with_vault( - accounts_path, - RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE), - ) - .map_err(|source| source.to_string()) - } - - #[cfg(target_os = "macos")] - fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState { - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => IdentityGateState::Missing, - RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, - RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }, - } - } - - #[cfg(target_os = "macos")] - fn account_roster_from_manager( - manager: &RadrootsNostrAccountsManager, - ) -> Result<Vec<RadrootsAccountSummary>, String> { - manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| { - let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?; - Ok(RadrootsAccountSummary { - account_id: record.account_id.to_string(), - npub: record.public_identity.public_key_npub, - label: record.label, - custody, - }) - }) - .collect() - } - - #[cfg(target_os = "macos")] - fn remove_selected_local_identity( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(IdentityGateState::Missing); - }; - - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) - } - - #[cfg(target_os = "macos")] - fn export_selected_local_encrypted_secret_key( - manager: &RadrootsNostrAccountsManager, - password: &str, - ) -> Result<String, String> { - Self::authorize_secret_key_backup()?; - - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - identity - .encrypt_secret_key_ncryptsec(password) - .map_err(|source| source.to_string()) - } - - #[cfg(target_os = "macos")] - fn export_selected_local_raw_secret_key( - manager: &RadrootsNostrAccountsManager, - ) -> Result<String, String> { - Self::authorize_secret_key_reveal()?; - - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - Ok(identity.nsec()) - } - - #[cfg(all(target_os = "macos", not(test)))] - fn authorize_secret_key_reveal() -> Result<(), String> { - verify_user_presence("reveal the current secret key").map_err(|source| source.to_string()) - } - - #[cfg(any(not(target_os = "macos"), test))] - fn authorize_secret_key_reveal() -> Result<(), String> { - Ok(()) - } - - #[cfg(all(target_os = "macos", not(test)))] - fn authorize_secret_key_backup() -> Result<(), String> { - verify_user_presence("back up the current secret key").map_err(|source| source.to_string()) - } - - #[cfg(any(not(target_os = "macos"), test))] - fn authorize_secret_key_backup() -> Result<(), String> { - Ok(()) - } - - #[cfg(target_os = "macos")] - fn import_local_identity( - manager: &RadrootsNostrAccountsManager, - request: &RadrootsSecretImportRequest, - ) -> Result<IdentityGateState, String> { - let identity = match request.mode { - RadrootsSecretImportMode::EncryptedSecretKey => { - let Some(password) = request.password.as_deref() else { - return Err("password is required to import an encrypted secret key".to_owned()); - }; - RadrootsIdentity::from_encrypted_secret_key_str( - request.secret_text.as_str(), - password, - ) - .map_err(|_| "invalid encrypted secret key or password".to_owned())? - } - RadrootsSecretImportMode::RawSecretKey => { - RadrootsIdentity::from_secret_key_str(request.secret_text.as_str()) - .map_err(|_| "invalid raw secret key".to_owned())? - } - }; - - manager - .upsert_identity(&identity, None, true) - .map_err(|source| source.to_string())?; - - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) - } - - #[cfg(target_os = "macos")] - fn remove_all_local_identities( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let account_ids = manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| record.account_id) - .collect::<Vec<_>>(); - - for account_id in account_ids { - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - } - - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) - } - - #[cfg(target_os = "macos")] - fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> { - match std::fs::remove_file(accounts_path) { - Ok(()) => Ok(()), - Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(source) => Err(format!("failed to remove accounts file: {source}")), - } - } - - #[cfg(target_os = "macos")] - fn reset_local_device_state( - manager: &RadrootsNostrAccountsManager, - accounts_path: &Path, - ) -> Result<IdentityGateState, String> { - remote_signer::purge_all_custody_state()?; - let state = Self::remove_all_local_identities(manager)?; - Self::remove_accounts_file_if_present(accounts_path)?; - Ok(state) - } -} - -impl RadrootsAppBackend for DesktopBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - return remote_signer::identity_state_from_status(status); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(IdentityGateState::Unsupported { - reason: "Local secure onboarding is only implemented for macOS in this slice." - .to_owned(), - }) - } - } - - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - return Self::account_roster_from_manager(&manager); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(Vec::new()) - } - } - - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - Some(self.offline_geocoder.current_state()) - } - - fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - Ok(self.offline_geocoder.take_update()) - } - - fn reverse_location( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::reverse_location( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - point, - options, - ); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = (point, options); - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn request_reverse_location_lookup( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return self.reverse_lookup.begin( - app_data_root, - self.offline_geocoder.current_state(), - point, - options, - ); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = (point, options); - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(self.reverse_lookup.take_update()) - } - - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return self - .country_lookup - .begin_list(app_data_root, self.offline_geocoder.current_state()); - } - - #[cfg(not(target_os = "macos"))] - { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_location_country_list_result( - &self, - ) -> Result<Option<RadrootsLocationCountryListResult>, String> { - Ok(self.country_lookup.take_list_update()) - } - - fn request_location_country_center_lookup( - &self, - country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return self.country_lookup.begin_center( - app_data_root, - self.offline_geocoder.current_state(), - country_id.to_owned(), - ); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = country_id; - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn poll_location_country_center_lookup_result( - &self, - ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { - Ok(self.country_lookup.take_center_update()) - } - - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::list_countries( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - ); - } - - #[cfg(not(target_os = "macos"))] - { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn location_country_center( - &self, - country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - #[cfg(target_os = "macos")] - { - let app_data_root = Self::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::country_center( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - country_id, - ); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = country_id; - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn setup_action_state(&self) -> SetupActionState { - #[cfg(target_os = "macos")] - { - return SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - }; - } - - #[cfg(not(target_os = "macos"))] - { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: false, - pending: false, - } - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - manager - .generate_identity(Some("local".to_owned()), true) - .map_err(|source| source.to_string())?; - return self.load_identity_state().map(Some); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(Some(IdentityGateState::Unsupported { - reason: "Local secure onboarding is only implemented for macOS in this slice." - .to_owned(), - })) - } - } - - fn home_setup_action_state(&self) -> Option<SetupActionState> { - Some(self.setup_action_state()) - } - - fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - self.request_setup_action() - } - - fn import_action_state(&self) -> Option<ImportActionState> { - #[cfg(target_os = "macos")] - { - return Some(ImportActionState { - label: "Import Secret Key".to_owned(), - enabled: true, - pending: false, - }); - } - - #[cfg(not(target_os = "macos"))] - { - None - } - } - - fn request_import_action( - &self, - request: &RadrootsSecretImportRequest, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - return Self::import_local_identity(&manager, request).map(Some); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = request; - Ok(None) - } - } - - fn remote_signer_action_state(&self) -> Option<SetupActionState> { - #[cfg(target_os = "macos")] - { - return Some( - self.remote_signer - .action_state() - .unwrap_or_else(|_| SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: !self.remote_signer.is_connecting(), - pending: self.remote_signer.is_connecting(), - }), - ); - } - - #[cfg(not(target_os = "macos"))] - { - None - } - } - - fn preview_remote_signer_connection( - &self, - input: &str, - ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> { - #[cfg(target_os = "macos")] - { - return remote_signer::preview_connection(input); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = input; - Err("remote signer onboarding is not available in this build".to_owned()) - } - } - - fn request_remote_signer_connection( - &self, - input: &str, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "macos")] - { - self.remote_signer.begin_connect(input)?; - return Ok(None); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = input; - Ok(None) - } - } - - fn pending_remote_signer_connection( - &self, - ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> { - #[cfg(target_os = "macos")] - { - return self.remote_signer.pending_connection(); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(None) - } - } - - fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> { - #[cfg(target_os = "macos")] - { - return remote_signer::cancel_pending_connection(); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(()) - } - } - - fn remote_signer_note_action_state(&self) -> Option<SetupActionState> { - #[cfg(target_os = "macos")] - { - return Some( - self.remote_signer - .note_action_state() - .unwrap_or(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }), - ); - } - - #[cfg(not(target_os = "macos"))] - { - None - } - } - - fn selected_remote_signer_approved_permissions(&self) -> Option<Vec<String>> { - #[cfg(target_os = "macos")] - { - return remote_signer::selected_approved_permission_labels().unwrap_or(None); - } - - #[cfg(not(target_os = "macos"))] - { - None - } - } - - fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> { - #[cfg(target_os = "macos")] - { - return self.remote_signer.begin_sign_kind1_note_selected(content); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = content; - Ok(()) - } - } - - fn poll_remote_signer_note_action_result( - &self, - ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> { - #[cfg(target_os = "macos")] - { - return self - .remote_signer - .take_note_update() - .transpose() - .map(|result| result.flatten()); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(None) - } - } - - fn request_select_account( - &self, - account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id) - .map_err(|_| "invalid account id".to_owned())?; - manager - .select_account(&account_id) - .map_err(|source| source.to_string())?; - return self.load_identity_state().map(Some); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = account_id; - Ok(None) - } - } - - fn home_action_states(&self) -> Vec<HomeActionState> { - #[cfg(target_os = "macos")] - { - let Ok(manager) = Self::accounts_manager() else { - return Vec::new(); - }; - let Ok(status) = manager - .selected_account_status() - .map_err(|source| source.to_string()) - else { - return Vec::new(); - }; - - return match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if matches!( - remote_signer::custody_for_account_id(account.account_id.as_str()), - Ok(RadrootsAccountCustody::RemoteSigner) - ) { - vec![HomeActionState { - kind: HomeActionKind::DisconnectSigner, - label: "Disconnect Remote Signer".to_owned(), - enabled: true, - pending: false, - }] - } else { - Vec::new() - } - } - RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![ - HomeActionState { - kind: HomeActionKind::BackupSecretKey, - label: "Back Up Secret Key".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::RevealRawSecretKey, - label: "Reveal Raw Secret Key".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::RemoveLocalKey, - label: "Remove Key From This Device".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::ResetDevice, - label: "Reset This Device".to_owned(), - enabled: true, - pending: false, - }, - ], - }; - } - - #[cfg(not(target_os = "macos"))] - { - Vec::new() - } - } - - fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - return match action { - HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None), - HomeActionKind::RevealRawSecretKey => { - Self::export_selected_local_raw_secret_key(&manager) - .map(|nsec| HomeActionResult::RevealRawSecretKey { nsec }) - } - HomeActionKind::RemoveLocalKey => Self::remove_selected_local_identity(&manager) - .map(HomeActionResult::IdentityState), - HomeActionKind::ResetDevice => { - let accounts_path = Self::accounts_path()?; - Self::reset_local_device_state(&manager, accounts_path.as_path()) - .map(HomeActionResult::IdentityState) - } - HomeActionKind::DisconnectSigner => { - remote_signer::disconnect_selected_remote_signer(&manager) - .map(HomeActionResult::IdentityState) - } - }; - } - - #[cfg(not(target_os = "macos"))] - { - let _ = action; - Ok(HomeActionResult::None) - } - } - - fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> { - #[cfg(target_os = "macos")] - { - let manager = Self::accounts_manager()?; - return Self::export_selected_local_encrypted_secret_key(&manager, password) - .map(|ncryptsec| HomeActionResult::RevealEncryptedSecretKey { ncryptsec }); - } - - #[cfg(not(target_os = "macos"))] - { - let _ = password; - Ok(HomeActionResult::None) - } - } - - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - #[cfg(target_os = "macos")] - { - return self - .remote_signer - .take_update() - .transpose() - .map(|state| state.flatten()); - } - - #[cfg(not(target_os = "macos"))] - { - Ok(None) - } - } -} - -fn main() -> eframe::Result<()> { - set_macos_app_name(); - - let viewport = { - let viewport = egui::ViewportBuilder::default() - .with_inner_size([1280.0, 820.0]) - .with_min_inner_size([480.0, 320.0]); - if let Some(icon) = desktop_icon() { - viewport.with_icon(icon) - } else { - viewport - } - }; - - let options = eframe::NativeOptions { - viewport, - ..Default::default() - }; - - eframe::run_native( - APP_NAME, - options, - Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(DesktopBackend::new()))))), - ) -} - -#[cfg(test)] -mod path_contract_tests { - use super::DesktopBackend; - use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform}; - use std::path::PathBuf; - - #[test] - fn desktop_app_paths_follow_linux_interactive_user_contract() { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment { - home_dir: Some(PathBuf::from("/home/treesap")), - ..RadrootsHostEnvironment::default() - }, - ); - - let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths"); - - assert_eq!( - paths.data, - PathBuf::from("/home/treesap/.radroots/data/apps/app") - ); - assert_eq!( - paths.logs, - PathBuf::from("/home/treesap/.radroots/logs/apps/app") - ); - assert_eq!( - paths.secrets, - PathBuf::from("/home/treesap/.radroots/secrets/apps/app") - ); - } - - #[test] - fn desktop_app_paths_follow_macos_interactive_user_contract() { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Macos, - RadrootsHostEnvironment { - home_dir: Some(PathBuf::from("/Users/treesap")), - ..RadrootsHostEnvironment::default() - }, - ); - - let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths"); - - assert_eq!( - paths.data, - PathBuf::from("/Users/treesap/.radroots/data/apps/app") - ); - assert_eq!( - paths.logs, - PathBuf::from("/Users/treesap/.radroots/logs/apps/app") - ); - assert_eq!( - paths.secrets, - PathBuf::from("/Users/treesap/.radroots/secrets/apps/app") - ); - } - - #[test] - fn desktop_app_paths_follow_windows_interactive_user_contract() { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Windows, - RadrootsHostEnvironment { - appdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Roaming")), - localappdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Local")), - ..RadrootsHostEnvironment::default() - }, - ); - - let paths = DesktopBackend::app_paths_with_resolver(&resolver).expect("desktop app paths"); - - assert_eq!( - paths.config, - PathBuf::from(r"C:\Users\treesap\AppData\Roaming") - .join("Radroots") - .join("config") - .join("apps") - .join("app") - ); - assert_eq!( - paths.data, - PathBuf::from(r"C:\Users\treesap\AppData\Local") - .join("Radroots") - .join("data") - .join("apps") - .join("app") - ); - assert_eq!( - paths.logs, - PathBuf::from(r"C:\Users\treesap\AppData\Local") - .join("Radroots") - .join("logs") - .join("apps") - .join("app") - ); - assert_eq!( - paths.secrets, - PathBuf::from(r"C:\Users\treesap\AppData\Roaming") - .join("Radroots") - .join("secrets") - .join("apps") - .join("app") - ); - } -} - -#[cfg(all(test, target_os = "macos"))] -mod tests { - use super::DesktopBackend; - use radroots_app_apple_security::RadrootsAppleKeychainVault; - use radroots_app_core::{ - IdentityGateState, RadrootsSecretImportMode, RadrootsSecretImportRequest, - }; - use radroots_app_test_support::{ - FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec, - }; - use radroots_identity::{RadrootsIdentity, RadrootsIdentityId}; - use radroots_nostr_accounts::prelude::{RadrootsSecretVault, account_secret_slot}; - use std::path::PathBuf; - - #[test] - fn private_directory_chain_covers_only_radroots_subtree() { - let root = PathBuf::from("/tmp/example/.radroots"); - let leaf = root.join("data").join("apps").join("app").join("nostr"); - - let chain = DesktopBackend::private_directory_chain(&root, &leaf).unwrap(); - - assert_eq!( - chain, - vec![ - PathBuf::from("/tmp/example/.radroots"), - PathBuf::from("/tmp/example/.radroots/data"), - PathBuf::from("/tmp/example/.radroots/data/apps"), - PathBuf::from("/tmp/example/.radroots/data/apps/app"), - PathBuf::from("/tmp/example/.radroots/data/apps/app/nostr"), - ] - ); - } - - #[test] - fn apple_keychain_vault_round_trips_secret_hex() { - let vault = - RadrootsAppleKeychainVault::new_desktop("org.radroots.app.tests.desktop.roundtrip"); - let account_id = RadrootsIdentityId::parse( - "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606", - ) - .expect("account id"); - let slot = account_secret_slot(&account_id); - - let _ = vault.remove_secret(slot.as_str()); - - vault - .store_secret( - slot.as_str(), - "a0468b0f2f5de9db868fb563b13632eb92ec4697dd4fddbdca0488f1a1b2c3d4", - ) - .expect("store secret"); - - assert_eq!( - vault.load_secret(slot.as_str()).expect("load secret"), - Some("a0468b0f2f5de9db868fb563b13632eb92ec4697dd4fddbdca0488f1a1b2c3d4".to_owned()) - ); - - vault.remove_secret(slot.as_str()).expect("remove secret"); - assert_eq!( - vault.load_secret(slot.as_str()).expect("load missing"), - None - ); - } - - #[test] - fn remove_all_local_identities_clears_every_account() { - let manager = - radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory(); - - manager - .generate_identity(Some("first".into()), true) - .expect("generate first"); - manager - .generate_identity(Some("second".into()), false) - .expect("generate second"); - - let state = DesktopBackend::remove_all_local_identities(&manager).expect("reset state"); - - assert_eq!(state, IdentityGateState::Missing); - assert_eq!(manager.list_accounts().expect("list accounts").len(), 0); - assert_eq!(manager.selected_account_id().expect("selected"), None); - } - - #[test] - fn export_selected_local_raw_secret_key_returns_nsec() { - let manager = - radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - manager - .upsert_identity(&identity, Some("primary".into()), true) - .expect("store identity"); - - let nsec = - DesktopBackend::export_selected_local_raw_secret_key(&manager).expect("export secret"); - - assert_eq!(nsec, identity.nsec()); - assert!(nsec.starts_with("nsec1")); - } - - #[test] - fn export_selected_local_encrypted_secret_key_returns_ncryptsec() { - let manager = - radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory(); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - - manager - .upsert_identity(&fixture_identity, Some("primary".into()), true) - .expect("store identity"); - - let ncryptsec = DesktopBackend::export_selected_local_encrypted_secret_key( - &manager, - FIXTURE_BACKUP_PASSWORD, - ) - .expect("export encrypted secret"); - - let restored = RadrootsIdentity::from_encrypted_secret_key_str( - ncryptsec.as_str(), - FIXTURE_BACKUP_PASSWORD, - ) - .expect("restore encrypted secret"); - - assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex); - } - - #[test] - fn import_local_identity_imports_raw_secret_key_and_selects_account() { - let manager = - radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - let state = DesktopBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::RawSecretKey, - secret_text: identity.nsec(), - password: None, - }, - ) - .expect("import identity"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: identity.id().to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(identity.id()) - ); - assert_eq!(manager.list_accounts().expect("list").len(), 1); - assert_eq!( - manager - .export_secret_hex(&identity.id()) - .expect("export secret"), - Some(identity.secret_key_hex()) - ); - } - - #[test] - fn import_local_identity_imports_encrypted_secret_key_and_selects_account() { - let manager = - radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager::new_in_memory(); - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - let fixture_account_id = fixture_identity.id(); - - let state = DesktopBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::EncryptedSecretKey, - secret_text: encrypted_secret_key, - password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()), - }, - ) - .expect("import identity"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: fixture_account_id.to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(fixture_account_id.clone()) - ); - assert_eq!( - manager - .export_secret_hex(&fixture_account_id) - .expect("export secret"), - Some(FIXTURE_ALICE.secret_key_hex.to_owned()) - ); - } - - #[test] - fn remove_accounts_file_if_present_deletes_existing_file() { - let unique = format!( - "radroots-desktop-reset-{}-{}.json", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time") - .as_nanos() - ); - let path = std::env::temp_dir().join(unique); - std::fs::write(&path, b"{}").expect("write accounts file"); - - DesktopBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file"); - - assert!(!path.exists()); - } -} diff --git a/crates/launchers/desktop/src/offline_geocoder.rs b/crates/launchers/desktop/src/offline_geocoder.rs @@ -1,514 +0,0 @@ -use radroots_app_core::{ - RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, -}; -use radroots_geocoder::{ - Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, - GeocoderReverseResult, -}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -const GEOCODER_ASSET_FILENAME: &str = "geonames.db"; -const GEOCODER_REVISION_FILENAME: &str = "geonames.revision"; - -#[derive(Clone)] -pub(crate) struct DesktopOfflineGeocoder { - current: Arc<Mutex<RadrootsOfflineGeocoderState>>, - changed: Arc<AtomicBool>, -} - -impl DesktopOfflineGeocoder { - pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self { - Self { - current: Arc::new(Mutex::new(state)), - changed: Arc::new(AtomicBool::new(false)), - } - } - - pub(crate) fn start(app_data_root: PathBuf) -> Self { - let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing); - - let current = Arc::clone(&tracker.current); - let changed = Arc::clone(&tracker.changed); - std::thread::spawn(move || { - let state = initialize_offline_geocoder(app_data_root.as_path()); - if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state { - log::warn!("desktop offline geocoder unavailable: {debug_message}"); - } - if let Ok(mut slot) = current.lock() { - *slot = state; - changed.store(true, Ordering::Release); - } - }); - - tracker - } - - pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState { - self.current - .lock() - .map(|state| state.clone()) - .unwrap_or_else(|_| { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Desktop, - "desktop offline geocoder state lock poisoned", - ) - }) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> { - if self.changed.swap(false, Ordering::AcqRel) { - Some(self.current_state()) - } else { - None - } - } -} - -pub(crate) fn reverse_location( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, -) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - let options = options.map(|options| GeocoderReverseOptions { - limit: options.limit, - degree_offset: options.degree_offset, - }); - geocoder - .reverse( - GeocoderPoint { - lat: point.lat, - lng: point.lng, - }, - options, - ) - .map(|results| results.into_iter().map(map_reverse_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -pub(crate) fn list_countries( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, -) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - geocoder - .country_list() - .map(|results| results.into_iter().map(map_country_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -pub(crate) fn country_center( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, - country_id: &str, -) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - geocoder - .country_center(country_id) - .map(|point| RadrootsLocationPoint { - lat: point.lat, - lng: point.lng, - }) - .map_err(map_country_center_error) -} - -fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { - let source_path = runtime_asset_path().map_err(|debug_message| { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Desktop, - debug_message, - ) - }); - let source_path = match source_path { - Ok(source_path) => source_path, - Err(state) => return state, - }; - if !source_path.is_file() { - return RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Desktop, - format!( - "desktop bundled geocoder asset missing at {}", - source_path.display() - ), - ); - } - - let revision = - match runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) { - Ok(revision) => revision, - Err((kind, debug_message)) => { - return RadrootsOfflineGeocoderState::unavailable( - kind, - RadrootsOfflineGeocoderPlatform::Desktop, - debug_message, - ); - } - }; - let staged_path = staged_db_path(app_data_root, revision.as_str()); - if let Err(debug_message) = stage_runtime_asset(source_path.as_path(), staged_path.as_path()) { - return RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Desktop, - revision, - debug_message, - ); - } - if let Err(source) = Geocoder::open_path(staged_path.as_path()) { - return RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Desktop, - revision, - format!("failed to open staged geocoder db: {source}"), - ); - } - let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str()); - RadrootsOfflineGeocoderState::Ready -} - -fn runtime_asset_path() -> Result<PathBuf, String> { - let executable_path = std::env::current_exe() - .map_err(|source| format!("failed to resolve desktop executable path: {source}"))?; - let Some(parent) = executable_path.parent() else { - return Err("desktop executable path did not have a parent directory".to_owned()); - }; - Ok(parent.join(GEOCODER_ASSET_FILENAME)) -} - -fn geocoder_for_queries( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, -) -> Result<Geocoder, RadrootsLocationResolverError> { - match state { - RadrootsOfflineGeocoderState::Initializing => { - return Err(RadrootsLocationResolverError::Initializing); - } - RadrootsOfflineGeocoderState::Unavailable { .. } => { - return Err(RadrootsLocationResolverError::Unavailable); - } - RadrootsOfflineGeocoderState::Ready => {} - } - - let source_path = runtime_asset_path() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - let revision = - runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) - .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?; - let staged_path = staged_db_path(app_data_root, revision.as_str()); - stage_runtime_asset(source_path.as_path(), staged_path.as_path()) - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - Geocoder::open_path(staged_path.as_path()).map_err(|source| { - RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - } - }) -} - -fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation { - RadrootsResolvedLocation { - id: result.id, - name: result.name, - admin1_id: result.admin1_id, - admin1_name: result.admin1_name, - country_id: result.country_id, - country_name: result.country_name, - point: RadrootsLocationPoint { - lat: result.latitude, - lng: result.longitude, - }, - } -} - -fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry { - RadrootsLocationCountry { - country_id: result.country_id, - country_name: result.country, - center: RadrootsLocationPoint { - lat: result.lat, - lng: result.lng, - }, - } -} - -fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError { - match source { - GeocoderError::CountryCenterNotFound { country_id } => { - RadrootsLocationResolverError::CountryCenterNotFound { country_id } - } - other => RadrootsLocationResolverError::QueryFailed { - message: other.to_string(), - }, - } -} - -fn runtime_asset_revision( - asset_dir: &Path, -) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> { - let revision_path = asset_dir.join(GEOCODER_REVISION_FILENAME); - let revision = std::fs::read_to_string(revision_path.as_path()).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - format!( - "desktop bundled geocoder revision asset missing at {}: {source}", - revision_path.display() - ), - ) - })?; - let revision = revision.trim(); - if !is_valid_revision(revision) { - return Err(( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - format!( - "desktop bundled geocoder revision asset invalid at {}", - revision_path.display() - ), - )); - } - Ok(revision.to_owned()) -} - -fn is_valid_revision(revision: &str) -> bool { - revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()) -} - -fn staged_geocoder_root(app_data_root: &Path) -> PathBuf { - app_data_root.join("geocoder") -} - -fn staged_db_path(app_data_root: &Path, revision: &str) -> PathBuf { - staged_geocoder_root(app_data_root) - .join(revision) - .join(GEOCODER_ASSET_FILENAME) -} - -fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> { - let Some(parent) = staged_path.parent() else { - return Err("staged desktop geocoder path did not have a parent directory".to_owned()); - }; - std::fs::create_dir_all(parent) - .map_err(|source| format!("failed to create desktop geocoder directory: {source}"))?; - if staged_path.is_file() { - return Ok(false); - } - std::fs::copy(source_path, staged_path) - .map_err(|source| format!("failed to stage desktop geocoder asset: {source}"))?; - Ok(true) -} - -fn prune_stale_revisions(staged_root: PathBuf, active_revision: &str) -> Result<(), String> { - if !staged_root.is_dir() { - return Ok(()); - } - - for entry in std::fs::read_dir(staged_root.as_path()) - .map_err(|source| format!("failed to list desktop geocoder revisions: {source}"))? - { - let entry = entry.map_err(|source| { - format!("failed to read desktop geocoder revision entry: {source}") - })?; - if entry.file_name() == std::ffi::OsStr::new(active_revision) { - continue; - } - - let path = entry.path(); - if entry - .file_type() - .map_err(|source| { - format!("failed to inspect desktop geocoder revision entry: {source}") - })? - .is_dir() - { - std::fs::remove_dir_all(path.as_path()).map_err(|source| { - format!( - "failed to remove stale desktop geocoder revision {}: {source}", - path.display() - ) - })?; - } else { - std::fs::remove_file(path.as_path()).map_err(|source| { - format!( - "failed to remove stale desktop geocoder revision file {}: {source}", - path.display() - ) - })?; - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[test] - fn staged_db_path_uses_app_geocoder_directory() { - let app_data_root = PathBuf::from("/Users/example/.radroots/data/apps/app"); - - assert_eq!( - staged_db_path(app_data_root.as_path(), "abcd"), - PathBuf::from("/Users/example/.radroots/data/apps/app/geocoder/abcd/geonames.db") - ); - } - - #[test] - fn valid_revision_requires_sha256_hex() { - assert!(is_valid_revision( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" - )); - assert!(!is_valid_revision("abcd")); - assert!(!is_valid_revision( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079z" - )); - } - - #[test] - fn missing_asset_maps_to_build_unavailable_message() { - let state = RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Desktop, - "desktop bundled geocoder asset missing at /tmp/geonames.db", - ); - - assert_eq!( - state, - RadrootsOfflineGeocoderState::Unavailable { - kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - platform: RadrootsOfflineGeocoderPlatform::Desktop, - asset_revision: None, - debug_message: "desktop bundled geocoder asset missing at /tmp/geonames.db" - .to_owned(), - } - ); - } - - #[test] - fn stage_runtime_asset_reuses_existing_staged_copy() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-desktop-geocoder-test-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let source_path = temp_root.join("source.db"); - let staged_path = temp_root.join("staged").join("geonames.db"); - - std::fs::create_dir_all(temp_root.as_path()).unwrap(); - std::fs::write(source_path.as_path(), b"source").unwrap(); - std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap(); - std::fs::write(staged_path.as_path(), b"existing").unwrap(); - - let copied = stage_runtime_asset(source_path.as_path(), staged_path.as_path()).unwrap(); - - assert!(!copied); - assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing"); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn prune_stale_revisions_keeps_active_revision_only() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-desktop-geocoder-prune-test-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let staged_root = temp_root.join("geocoder"); - let active_dir = staged_root.join("active"); - let stale_dir = staged_root.join("stale"); - let stale_file = staged_root.join("orphan.txt"); - - std::fs::create_dir_all(active_dir.as_path()).unwrap(); - std::fs::create_dir_all(stale_dir.as_path()).unwrap(); - std::fs::write(active_dir.join("geonames.db"), b"active").unwrap(); - std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap(); - std::fs::write(stale_file.as_path(), b"orphan").unwrap(); - - prune_stale_revisions(staged_root.clone(), "active").unwrap(); - - assert!(active_dir.exists()); - assert!(!stale_dir.exists()); - assert!(!stale_file.exists()); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn runtime_asset_revision_reads_stamped_sidecar() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-desktop-geocoder-revision-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let revision_path = temp_root.join(GEOCODER_REVISION_FILENAME); - let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"; - - std::fs::create_dir_all(temp_root.as_path()).unwrap(); - std::fs::write(revision_path.as_path(), format!("{revision}\n")).unwrap(); - - assert_eq!( - runtime_asset_revision(temp_root.as_path()).unwrap(), - revision.to_owned() - ); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn reverse_result_mapping_preserves_location_fields() { - let mapped = map_reverse_result(GeocoderReverseResult { - id: 42, - name: "Oslo".to_owned(), - admin1_id: Some(12), - admin1_name: Some("Oslo".to_owned()), - country_id: "NO".to_owned(), - country_name: Some("Norway".to_owned()), - latitude: 59.9139, - longitude: 10.7522, - }); - - assert_eq!(mapped.id, 42); - assert_eq!(mapped.name, "Oslo"); - assert_eq!(mapped.admin1_id, Some(12)); - assert_eq!(mapped.admin1_name.as_deref(), Some("Oslo")); - assert_eq!(mapped.country_id, "NO"); - assert_eq!(mapped.country_name.as_deref(), Some("Norway")); - assert_eq!(mapped.point.lat, 59.9139); - assert_eq!(mapped.point.lng, 10.7522); - } - - #[test] - fn unavailable_state_blocks_queries_until_ready() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-desktop-geocoder-query-state-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - - let result = list_countries( - temp_root.as_path(), - &RadrootsOfflineGeocoderState::Initializing, - ); - - assert_eq!(result, Err(RadrootsLocationResolverError::Initializing)); - } -} diff --git a/crates/launchers/desktop/src/remote_signer.rs b/crates/launchers/desktop/src/remote_signer.rs @@ -1,518 +0,0 @@ -use super::DesktopBackend; -use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault}; -use radroots_app_core::{ - IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection, - RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState, -}; -use radroots_app_remote_signer::{ - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController, - RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState, - RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, - RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session, - radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, - radroots_app_remote_signer_purge_all_custody_state, - radroots_app_remote_signer_reconcile_startup, -}; -use radroots_identity::RadrootsIdentityId; -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault, - account_secret_slot, -}; -use std::path::{Path, PathBuf}; - -const REMOTE_SIGNER_LABEL: &str = "remote signer"; - -#[derive(Clone, Copy)] -struct DesktopRemoteSignerHooks; - -impl RadrootsAppRemoteSignerControllerHooks for DesktopRemoteSignerHooks { - type ReadyState = IdentityGateState; - - fn reconcile_startup_state(&self) -> Result<(), String> { - let manager = DesktopBackend::accounts_manager()?; - let store_path = sessions_path()?; - radroots_app_remote_signer_reconcile_startup( - &manager, - store_path.as_path(), - REMOTE_SIGNER_LABEL, - load_client_secret, - remove_client_secret, - purge_client_secret_namespace, - ) - } - - fn store_pending_session( - &self, - pending: &RadrootsAppRemoteSignerPendingSession, - ) -> Result<(), String> { - let client_account_id = pending.record.client_account_id().to_owned(); - store_client_secret( - client_account_id.as_str(), - pending.client_secret_key_hex.as_str(), - )?; - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - if let Err(error) = state.upsert_pending(pending.record.clone()) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error.to_string()); - } - if let Err(error) = save_sessions(store_path.as_path(), &state) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error); - } - Ok(()) - } - - fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - pending_session_record() - } - - fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> { - load_client_secret(client_account_id) - } - - fn activate_pending_session( - &self, - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, - ) -> Result<Self::ReadyState, String> { - activate_remote_session(client_account_id, approved) - } - - fn clear_pending_session( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret) - } -} - -#[derive(Clone)] -pub(crate) struct DesktopRemoteSigner { - controller: RadrootsAppRemoteSignerController<DesktopRemoteSignerHooks>, - action_controller: RadrootsAppRemoteSignerActionController<DesktopRemoteSignerHooks>, -} - -impl DesktopRemoteSigner { - pub(crate) fn new() -> Self { - Self { - controller: RadrootsAppRemoteSignerController::new(DesktopRemoteSignerHooks), - action_controller: RadrootsAppRemoteSignerActionController::new( - DesktopRemoteSignerHooks, - ), - } - } - - pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> { - self.controller.take_update() - } - - pub(crate) fn is_connecting(&self) -> bool { - self.controller.is_connecting() - } - - pub(crate) fn action_state(&self) -> Result<SetupActionState, String> { - if self.is_connecting() { - return Ok(SetupActionState { - label: "Connecting Remote Signer...".to_owned(), - enabled: false, - pending: true, - }); - } - - if self.pending_connection()?.is_some() { - return Ok(match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { - label: "Remote Signer Approval Check Retrying".to_owned(), - enabled: false, - pending: false, - }, - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => { - SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - } - } - RadrootsAppRemoteSignerPendingState::Idle - | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, - }, - }); - } - - Ok(SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: true, - pending: false, - }) - } - - pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> { - self.controller.begin_connect(input) - } - - pub(crate) fn pending_connection( - &self, - ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> { - Ok( - pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection { - signer_npub: record.signer_identity.public_key_npub, - relays: record.relays, - auth_url: match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url), - _ => None, - }, - }), - ) - } - - pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Ok(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }); - }; - let Some(record) = active_session_for_account_id(account_id.as_str())? else { - return Ok(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }); - }; - if !record.allows_sign_event_kind1() { - return Ok(SetupActionState { - label: "Remote Signer Missing sign_event:kind:1".to_owned(), - enabled: false, - pending: false, - }); - } - - Ok(match self.action_controller.state() { - RadrootsAppRemoteSignerActionState::Idle => SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: true, - pending: false, - }, - RadrootsAppRemoteSignerActionState::Signing => SetupActionState { - label: "Signing Remote Kind 1 Note...".to_owned(), - enabled: false, - pending: true, - }, - RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - }, - }) - } - - pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> { - self.action_controller.begin_sign_kind1_note(content) - } - - pub(crate) fn take_note_update( - &self, - ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> { - self.action_controller.take_update() - } -} - -pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { - let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; - let requested_permissions = preview.requested_permission_labels(); - Ok(RadrootsRemoteSignerPreview { - source_label: preview.source_label().to_owned(), - signer_npub: preview.signer_identity.public_key_npub, - relays: preview.relays, - requested_permissions, - }) -} - -pub(crate) fn identity_state_from_status( - status: RadrootsNostrSelectedAccountStatus, -) -> Result<IdentityGateState, String> { - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing), - RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if active_session_for_account_id(account.account_id.as_str())?.is_some() { - Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }) - } else { - Ok(IdentityGateState::Missing) - } - } - } -} - -pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> { - if active_session_for_account_id(account_id)?.is_some() { - Ok(RadrootsAccountCustody::RemoteSigner) - } else { - Ok(RadrootsAccountCustody::LocalManaged) - } -} - -pub(crate) fn disconnect_selected_remote_signer( - manager: &RadrootsNostrAccountsManager, -) -> Result<IdentityGateState, String> { - let store_path = sessions_path()?; - let status = radroots_app_remote_signer_disconnect_selected( - manager, - store_path.as_path(), - remove_client_secret, - )?; - identity_state_from_status(status) -} - -pub(crate) fn cancel_pending_connection() -> Result<(), String> { - let store_path = sessions_path()?; - let _ = radroots_app_remote_signer_clear_pending_session( - store_path.as_path(), - remove_client_secret, - )?; - Ok(()) -} - -pub(crate) fn purge_all_custody_state() -> Result<(), String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_purge_all_custody_state( - store_path.as_path(), - remove_client_secret, - purge_client_secret_namespace, - ) -} - -fn activate_remote_session( - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, -) -> Result<IdentityGateState, String> { - let manager = DesktopBackend::accounts_manager()?; - manager - .upsert_public_identity( - approved.user_identity.clone(), - Some(REMOTE_SIGNER_LABEL.to_owned()), - true, - ) - .map_err(|source| source.to_string())?; - let store_path = sessions_path()?; - let activation_result = (|| -> Result<(), String> { - let mut state = load_sessions(store_path.as_path())?; - state - .activate_session( - client_account_id, - approved.user_identity.clone(), - approved.relays.clone(), - approved.approved_permissions.clone(), - ) - .ok_or_else(|| { - "pending remote signer session disappeared before activation".to_owned() - })?; - save_sessions(store_path.as_path(), &state) - })(); - if let Err(error) = activation_result { - if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) { - return Err(format!( - "{error}. remote signer account rollback needs retry: {rollback_error}" - )); - } - return Err(error); - } - Ok(IdentityGateState::Ready { - account_id: approved.user_identity.id.to_string(), - }) -} - -fn selected_remote_signer_account() -> Result<Option<String>, String> { - let manager = DesktopBackend::accounts_manager()?; - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(None); - }; - if active_session_for_account_id(account_id.as_str())?.is_some() { - Ok(Some(account_id.to_string())) - } else { - Ok(None) - } -} - -fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> { - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else { - return Err("active remote signer session disappeared before relay update".to_owned()); - }; - if session.relays == relays { - return Ok(()); - } - session.relays = relays; - state.remove_active_session_for_account_id(account_id); - state.sessions.push(session); - save_sessions(store_path.as_path(), &state) -} - -impl RadrootsAppRemoteSignerActionControllerHooks for DesktopRemoteSignerHooks { - type ReadyState = RadrootsRemoteSignerSignedNote; - - fn selected_active_session( - &self, - ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Ok(None); - }; - let Some(record) = active_session_for_account_id(account_id.as_str())? else { - return Ok(None); - }; - if !record.allows_sign_event_kind1() { - return Err("remote signer has not approved sign_event:kind:1".to_owned()); - } - let secret = load_client_secret(record.client_account_id())?; - Ok(Some((record, secret))) - } - - fn complete_sign_event( - &self, - signed_event: RadrootsAppRemoteSignerSignedEvent, - ) -> Result<Self::ReadyState, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Err("remote signer account is no longer selected".to_owned()); - }; - update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?; - Ok(RadrootsRemoteSignerSignedNote { - event_id_hex: signed_event.event_id_hex, - }) - } -} - -fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.pending_session().cloned()) -} - -fn active_session_for_account_id( - account_id: &str, -) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.active_session_for_account_id(account_id).cloned()) -} - -pub(crate) fn selected_approved_permission_labels() -> Result<Option<Vec<String>>, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Ok(None); - }; - Ok(active_session_for_account_id(account_id.as_str())? - .map(|record| record.approved_permission_labels())) -} - -fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> { - RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string()) -} - -fn save_sessions( - path: &Path, - state: &RadrootsAppRemoteSignerSessionStoreState, -) -> Result<(), String> { - state.save(path).map_err(|error| error.to_string()) -} - -fn sessions_path() -> Result<PathBuf, String> { - Ok(DesktopBackend::app_data_root()? - .join("nostr") - .join("remote-signer-sessions.json")) -} - -fn client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_with_namespace_desktop( - APPLE_NOSTR_SERVICE, - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, - ) -} - -fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE) -} - -fn client_secret_slot(client_account_id: &str) -> Result<String, String> { - let account_id = RadrootsIdentityId::try_from(client_account_id) - .map_err(|_| "invalid remote signer client account id".to_owned())?; - Ok(account_secret_slot(&account_id)) -} - -fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .store_secret(slot.as_str(), secret_key_hex) - .map_err(|source| source.to_string()) -} - -fn load_client_secret(client_account_id: &str) -> Result<String, String> { - let slot = client_secret_slot(client_account_id)?; - if let Some(secret) = client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - { - return Ok(secret); - } - - let secret = legacy_client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - .ok_or_else(|| "remote signer session secret is missing".to_owned())?; - let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str()); - let _ = legacy_client_secret_vault().remove_secret(slot.as_str()); - Ok(secret) -} - -fn remove_client_secret(client_account_id: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string())?; - legacy_client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string()) -} - -fn purge_client_secret_namespace() -> Result<(), String> { - client_secret_vault() - .purge_namespace() - .map_err(|source| source.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::FIXTURE_BOB; - - #[test] - fn preview_connection_maps_signer_details() { - let preview = preview_connection( - "http://localhost/connect?uri=bunker%3A%2F%2Fnpub1uqnxu08mp55gd7guw06ls68nhxp8xuf7tlxe0sypvcl42x9ykwhsd55k2g%3Frelay%3Dws%253A%252F%252Flocalhost%253A8080", - ) - .expect("preview"); - - assert_eq!(preview.source_label, "discovery url"); - assert_eq!(preview.signer_npub, FIXTURE_BOB.npub); - assert_eq!(preview.relays, vec!["ws://localhost:8080".to_owned()]); - assert_eq!( - preview.requested_permissions, - vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned(),] - ); - } -} diff --git a/crates/launchers/desktop/src/reverse_lookup.rs b/crates/launchers/desktop/src/reverse_lookup.rs @@ -1,119 +0,0 @@ -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult, -}; -#[cfg(target_os = "macos")] -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct DesktopReverseLookup { - result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>, - changed: Arc<AtomicBool>, - pending: Arc<AtomicBool>, -} - -impl DesktopReverseLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "macos")] - pub(crate) fn begin( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - if self.pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline location query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.result); - let changed = Arc::clone(&self.changed); - let pending = Arc::clone(&self.pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::reverse_location( - app_data_root.as_path(), - &geocoder_state, - point, - options, - ); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "macos"))] - pub(crate) fn begin( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> { - if !self.changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "desktop reverse lookup result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::RadrootsResolvedLocation; - - fn sample_result() -> RadrootsReverseLocationLookupResult { - Ok(vec![RadrootsResolvedLocation { - id: 7, - name: "example".to_owned(), - admin1_id: None, - admin1_name: None, - country_id: "US".to_owned(), - country_name: Some("United States".to_owned()), - point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 }, - }]) - } - - #[test] - fn take_update_is_none_until_tracker_changes() { - let tracker = DesktopReverseLookup::new(); - - assert_eq!(tracker.take_update(), None); - } - - #[test] - fn take_update_returns_queued_result_once() { - let tracker = DesktopReverseLookup::new(); - *tracker.result.lock().unwrap() = Some(sample_result()); - tracker.changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_update(), None); - } -} diff --git a/crates/launchers/ios/Cargo.toml b/crates/launchers/ios/Cargo.toml @@ -1,33 +0,0 @@ -[package] -name = "radroots_app_ios" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots iOS launcher" -publish = false - -[lib] -path = "src/lib.rs" -crate-type = ["staticlib", "rlib"] - -[dependencies] -eframe = { workspace = true, features = ["wgpu"] } -log.workspace = true -radroots_app_apple_security.workspace = true -radroots_app_core = { path = "../../shared/core" } -radroots_app_remote_signer = { path = "../../shared/remote_signer" } -radroots_geocoder.workspace = true -radroots_identity.workspace = true -radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] } -radroots_runtime_paths.workspace = true -zeroize.workspace = true - -[target.'cfg(target_os = "ios")'.dependencies] -wgpu = { workspace = true, features = ["metal", "wgsl"] } - -[dev-dependencies] -radroots_app_test_support = { path = "../../shared/test_support" } diff --git a/crates/launchers/ios/src/country_lookup.rs b/crates/launchers/ios/src/country_lookup.rs @@ -1,190 +0,0 @@ -#![cfg_attr(not(target_os = "ios"), allow(dead_code))] - -#[cfg(target_os = "ios")] -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationResolverError, RadrootsOfflineGeocoderState, -}; -#[cfg(target_os = "ios")] -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct IosCountryLookup { - country_list_result: Arc<Mutex<Option<RadrootsLocationCountryListResult>>>, - country_list_changed: Arc<AtomicBool>, - country_list_pending: Arc<AtomicBool>, - country_center_result: Arc<Mutex<Option<RadrootsLocationCountryCenterLookupResult>>>, - country_center_changed: Arc<AtomicBool>, - country_center_pending: Arc<AtomicBool>, -} - -impl IosCountryLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "ios")] - pub(crate) fn begin_list( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_list_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country list query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_list_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_list_result); - let changed = Arc::clone(&self.country_list_changed); - let pending = Arc::clone(&self.country_list_pending); - std::thread::spawn(move || { - let lookup_result = - offline_geocoder::list_countries(app_data_root.as_path(), &geocoder_state); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "ios"))] - pub(crate) fn begin_list( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - #[cfg(target_os = "ios")] - pub(crate) fn begin_center( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - if self.country_center_pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline country center query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.country_center_result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.country_center_result); - let changed = Arc::clone(&self.country_center_changed); - let pending = Arc::clone(&self.country_center_pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::country_center( - app_data_root.as_path(), - &geocoder_state, - &country_id, - ); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "ios"))] - pub(crate) fn begin_center( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - _country_id: String, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_list_update(&self) -> Option<RadrootsLocationCountryListResult> { - if !self.country_list_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_list_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "ios country list result lock poisoned".to_owned(), - })), - } - } - - pub(crate) fn take_center_update(&self) -> Option<RadrootsLocationCountryCenterLookupResult> { - if !self.country_center_changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.country_center_result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "ios country center result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::{RadrootsLocationCountry, RadrootsLocationPoint}; - - fn sample_countries() -> RadrootsLocationCountryListResult { - Ok(vec![RadrootsLocationCountry { - country_id: "BR".to_owned(), - country_name: Some("Brazil".to_owned()), - center: RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - }, - }]) - } - - #[test] - fn take_list_update_is_none_until_tracker_changes() { - let tracker = IosCountryLookup::new(); - - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_list_update_returns_queued_result_once() { - let tracker = IosCountryLookup::new(); - *tracker.country_list_result.lock().unwrap() = Some(sample_countries()); - tracker.country_list_changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_list_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_list_update(), None); - } - - #[test] - fn take_center_update_returns_queued_result_once() { - let tracker = IosCountryLookup::new(); - *tracker.country_center_result.lock().unwrap() = Some(Ok(RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - })); - tracker - .country_center_changed - .store(true, Ordering::Release); - - assert!(matches!(tracker.take_center_update(), Some(Ok(point)) if point.lat == -14.235)); - assert_eq!(tracker.take_center_update(), None); - } -} diff --git a/crates/launchers/ios/src/lib.rs b/crates/launchers/ios/src/lib.rs @@ -1,965 +0,0 @@ -#[cfg(target_os = "ios")] -use eframe::egui::ViewportBuilder; -#[cfg(target_os = "ios")] -use radroots_app_apple_security::verify_user_presence; -#[cfg(any(target_os = "ios", test))] -use radroots_app_core::IdentityGateState; -#[cfg(target_os = "ios")] -use radroots_app_core::{ - APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, ImportActionState, - PasteActionState, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry, - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, - RadrootsReverseLocationLookupResult, SetupActionState, -}; -#[cfg(any(target_os = "ios", test))] -use radroots_app_core::{RadrootsAccountCustody, RadrootsAccountSummary}; -#[cfg(any(target_os = "ios", test))] -use radroots_app_core::{RadrootsSecretImportMode, RadrootsSecretImportRequest}; -#[cfg(any(target_os = "ios", test))] -use radroots_identity::RadrootsIdentity; -#[cfg(any(target_os = "ios", test))] -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, -}; -#[cfg(any(target_os = "ios", test))] -use std::path::Path; -#[cfg(any(target_os = "ios", test))] -use zeroize::Zeroizing; - -#[cfg(any(target_os = "ios", test))] -mod country_lookup; -#[cfg(any(target_os = "ios", test))] -mod offline_geocoder; -#[cfg(target_os = "ios")] -mod remote_signer; -#[cfg(any(target_os = "ios", test))] -mod reverse_lookup; -#[cfg(any(target_os = "ios", test))] -mod storage; - -#[cfg(any(target_os = "ios", test))] -#[cfg_attr(not(target_os = "ios"), allow(dead_code))] -struct IosBackend { - country_lookup: country_lookup::IosCountryLookup, - offline_geocoder: offline_geocoder::IosOfflineGeocoder, - #[cfg(target_os = "ios")] - remote_signer: remote_signer::IosRemoteSigner, - reverse_lookup: reverse_lookup::IosReverseLookup, -} - -#[cfg(target_os = "ios")] -#[allow(unsafe_code)] -unsafe extern "C" { - fn radroots_ios_clipboard_text_copy() -> *mut std::ffi::c_char; - fn radroots_ios_string_free(value: *mut std::ffi::c_char); -} - -#[cfg(any(target_os = "ios", test))] -impl IosBackend { - #[cfg(target_os = "ios")] - fn new() -> Self { - let offline_geocoder = match storage::app_data_root() { - Ok(app_data_root) => offline_geocoder::IosOfflineGeocoder::start(app_data_root), - Err(debug_message) => offline_geocoder::IosOfflineGeocoder::from_state( - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Ios, - debug_message, - ), - ), - }; - - Self { - country_lookup: country_lookup::IosCountryLookup::new(), - offline_geocoder, - #[cfg(target_os = "ios")] - remote_signer: remote_signer::IosRemoteSigner::new(), - reverse_lookup: reverse_lookup::IosReverseLookup::new(), - } - } - - #[cfg(target_os = "ios")] - fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { - storage::accounts_manager() - } - - fn map_status(status: RadrootsNostrSelectedAccountStatus) -> IdentityGateState { - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => IdentityGateState::Missing, - RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => IdentityGateState::Missing, - RadrootsNostrSelectedAccountStatus::Ready { account } => IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }, - } - } - - fn identity_state_from_manager( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - Ok(Self::map_status(status)) - } - - fn account_roster_from_manager( - manager: &RadrootsNostrAccountsManager, - ) -> Result<Vec<RadrootsAccountSummary>, String> { - manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| { - #[cfg(target_os = "ios")] - let custody = remote_signer::custody_for_account_id(record.account_id.as_str())?; - #[cfg(not(target_os = "ios"))] - let custody = RadrootsAccountCustody::LocalManaged; - Ok(RadrootsAccountSummary { - account_id: record.account_id.to_string(), - npub: record.public_identity.public_key_npub, - label: record.label, - custody, - }) - }) - .collect() - } - - fn generate_local_identity( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - manager - .generate_identity(Some("local".to_owned()), true) - .map_err(|source| source.to_string())?; - Self::identity_state_from_manager(manager) - } - - fn remove_selected_local_identity( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(IdentityGateState::Missing); - }; - - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - Self::identity_state_from_manager(manager) - } - - fn export_selected_local_encrypted_secret_key( - manager: &RadrootsNostrAccountsManager, - password: &str, - ) -> Result<String, String> { - Self::authorize_secret_key_backup()?; - - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - identity - .encrypt_secret_key_ncryptsec(password) - .map_err(|source| source.to_string()) - } - - fn export_selected_local_raw_secret_key( - manager: &RadrootsNostrAccountsManager, - ) -> Result<String, String> { - Self::authorize_secret_key_reveal()?; - - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Err("no selected local identity is available to back up".to_owned()); - }; - - let Some(secret_key_hex) = manager - .export_secret_hex(&account_id) - .map_err(|source| source.to_string())? - else { - return Err("selected local identity does not have an exportable secret".to_owned()); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str()) - .map_err(|source| source.to_string())?; - Ok(identity.nsec()) - } - - fn import_local_identity( - manager: &RadrootsNostrAccountsManager, - request: &RadrootsSecretImportRequest, - ) -> Result<IdentityGateState, String> { - let identity = match request.mode { - RadrootsSecretImportMode::EncryptedSecretKey => { - let Some(password) = request.password.as_deref() else { - return Err("password is required to import an encrypted secret key".to_owned()); - }; - RadrootsIdentity::from_encrypted_secret_key_str( - request.secret_text.as_str(), - password, - ) - .map_err(|_| "invalid encrypted secret key or password".to_owned())? - } - RadrootsSecretImportMode::RawSecretKey => { - RadrootsIdentity::from_secret_key_str(request.secret_text.as_str()) - .map_err(|_| "invalid raw secret key".to_owned())? - } - }; - - manager - .upsert_identity(&identity, None, true) - .map_err(|source| source.to_string())?; - - Self::identity_state_from_manager(manager) - } - - fn normalize_clipboard_secret_key_text(clipboard_text: &str) -> Result<String, String> { - let trimmed = clipboard_text.trim(); - if trimmed.is_empty() { - return Err("clipboard does not contain text".to_owned()); - } - - Ok(match trimmed.len() == clipboard_text.len() { - true => clipboard_text.to_owned(), - false => trimmed.to_owned(), - }) - } - - #[cfg(target_os = "ios")] - #[allow(unsafe_code)] - fn paste_secret_key_from_clipboard() -> Result<String, String> { - let clipboard_text_ptr = unsafe { radroots_ios_clipboard_text_copy() }; - if clipboard_text_ptr.is_null() { - return Err("clipboard does not contain text".to_owned()); - } - - let clipboard_text = unsafe { - let value = std::ffi::CStr::from_ptr(clipboard_text_ptr) - .to_string_lossy() - .into_owned(); - radroots_ios_string_free(clipboard_text_ptr); - value - }; - - Self::normalize_clipboard_secret_key_text(&clipboard_text) - } - - #[cfg(target_os = "ios")] - fn authorize_secret_key_reveal() -> Result<(), String> { - verify_user_presence("reveal the current secret key").map_err(|source| source.to_string()) - } - - #[cfg(target_os = "ios")] - fn authorize_secret_key_backup() -> Result<(), String> { - verify_user_presence("back up the current secret key").map_err(|source| source.to_string()) - } - - #[cfg(not(target_os = "ios"))] - fn authorize_secret_key_reveal() -> Result<(), String> { - Ok(()) - } - - #[cfg(not(target_os = "ios"))] - fn authorize_secret_key_backup() -> Result<(), String> { - Ok(()) - } - - fn remove_all_local_identities( - manager: &RadrootsNostrAccountsManager, - ) -> Result<IdentityGateState, String> { - let account_ids = manager - .list_accounts() - .map_err(|source| source.to_string())? - .into_iter() - .map(|record| record.account_id) - .collect::<Vec<_>>(); - - for account_id in account_ids { - manager - .remove_account(&account_id) - .map_err(|source| source.to_string())?; - } - - Self::identity_state_from_manager(manager) - } - - fn remove_accounts_file_if_present(accounts_path: &Path) -> Result<(), String> { - match std::fs::remove_file(accounts_path) { - Ok(()) => Ok(()), - Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(source) => Err(format!("failed to remove ios accounts file: {source}")), - } - } - - #[cfg(target_os = "ios")] - fn reset_local_device_state( - manager: &RadrootsNostrAccountsManager, - accounts_path: &Path, - ) -> Result<IdentityGateState, String> { - remote_signer::purge_all_custody_state()?; - let state = Self::remove_all_local_identities(manager)?; - Self::remove_accounts_file_if_present(accounts_path)?; - Ok(state) - } -} - -#[cfg(target_os = "ios")] -impl RadrootsAppBackend for IosBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - let manager = Self::accounts_manager()?; - let status = manager - .selected_account_status() - .map_err(|source| source.to_string())?; - remote_signer::identity_state_from_status(status) - } - - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - let manager = Self::accounts_manager()?; - Self::account_roster_from_manager(&manager) - } - - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - Some(self.offline_geocoder.current_state()) - } - - fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - Ok(self.offline_geocoder.take_update()) - } - - fn reverse_location( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - #[cfg(target_os = "ios")] - { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::reverse_location( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - point, - options, - ); - } - - #[cfg(not(target_os = "ios"))] - { - let _ = (point, options); - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn request_reverse_location_lookup( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - self.reverse_lookup.begin( - app_data_root, - self.offline_geocoder.current_state(), - point, - options, - ) - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(self.reverse_lookup.take_update()) - } - - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - self.country_lookup - .begin_list(app_data_root, self.offline_geocoder.current_state()) - } - - fn poll_location_country_list_result( - &self, - ) -> Result<Option<RadrootsLocationCountryListResult>, String> { - Ok(self.country_lookup.take_list_update()) - } - - fn request_location_country_center_lookup( - &self, - country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - self.country_lookup.begin_center( - app_data_root, - self.offline_geocoder.current_state(), - country_id.to_owned(), - ) - } - - fn poll_location_country_center_lookup_result( - &self, - ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { - Ok(self.country_lookup.take_center_update()) - } - - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - #[cfg(target_os = "ios")] - { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::list_countries( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - ); - } - - #[cfg(not(target_os = "ios"))] - { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn location_country_center( - &self, - country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - #[cfg(target_os = "ios")] - { - let app_data_root = storage::app_data_root() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - return offline_geocoder::country_center( - app_data_root.as_path(), - &self.offline_geocoder.current_state(), - country_id, - ); - } - - #[cfg(not(target_os = "ios"))] - { - let _ = country_id; - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn setup_action_state(&self) -> SetupActionState { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - let manager = Self::accounts_manager()?; - Self::generate_local_identity(&manager).map(Some) - } - - fn home_setup_action_state(&self) -> Option<SetupActionState> { - Some(self.setup_action_state()) - } - - fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - self.request_setup_action() - } - - fn import_action_state(&self) -> Option<ImportActionState> { - Some(ImportActionState { - label: "Import Secret Key".to_owned(), - enabled: true, - pending: false, - }) - } - - fn request_import_action( - &self, - request: &RadrootsSecretImportRequest, - ) -> Result<Option<IdentityGateState>, String> { - let manager = Self::accounts_manager()?; - Self::import_local_identity(&manager, request).map(Some) - } - - fn request_select_account( - &self, - account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - let manager = Self::accounts_manager()?; - let account_id = radroots_identity::RadrootsIdentityId::try_from(account_id) - .map_err(|_| "invalid account id".to_owned())?; - manager - .select_account(&account_id) - .map_err(|source| source.to_string())?; - self.load_identity_state().map(Some) - } - - fn remote_signer_action_state(&self) -> Option<SetupActionState> { - Some( - self.remote_signer - .action_state() - .unwrap_or_else(|_| SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: !self.remote_signer.is_connecting(), - pending: self.remote_signer.is_connecting(), - }), - ) - } - - fn preview_remote_signer_connection( - &self, - input: &str, - ) -> Result<radroots_app_core::RadrootsRemoteSignerPreview, String> { - remote_signer::preview_connection(input) - } - - fn request_remote_signer_connection( - &self, - input: &str, - ) -> Result<Option<IdentityGateState>, String> { - self.remote_signer.begin_connect(input)?; - Ok(None) - } - - fn pending_remote_signer_connection( - &self, - ) -> Result<Option<radroots_app_core::RadrootsPendingRemoteSignerConnection>, String> { - self.remote_signer.pending_connection() - } - - fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> { - remote_signer::cancel_pending_connection() - } - - fn remote_signer_note_action_state(&self) -> Option<SetupActionState> { - Some( - self.remote_signer - .note_action_state() - .unwrap_or(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }), - ) - } - - fn request_remote_signer_note_action(&self, content: &str) -> Result<(), String> { - self.remote_signer.begin_sign_kind1_note_selected(content) - } - - fn poll_remote_signer_note_action_result( - &self, - ) -> Result<Option<radroots_app_core::RadrootsRemoteSignerSignedNote>, String> { - self.remote_signer - .take_note_update() - .transpose() - .map(|result| result.flatten()) - } - - fn import_paste_action_state(&self) -> Option<PasteActionState> { - Some(PasteActionState { - label: "Paste Secret Key".to_owned(), - enabled: true, - pending: false, - }) - } - - fn request_import_paste_action(&self) -> Result<Option<String>, String> { - Self::paste_secret_key_from_clipboard().map(Some) - } - - fn home_action_states(&self) -> Vec<HomeActionState> { - let Ok(manager) = Self::accounts_manager() else { - return Vec::new(); - }; - let Ok(status) = manager - .selected_account_status() - .map_err(|source| source.to_string()) - else { - return Vec::new(); - }; - - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Vec::new(), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if matches!( - remote_signer::custody_for_account_id(account.account_id.as_str()), - Ok(RadrootsAccountCustody::RemoteSigner) - ) { - vec![HomeActionState { - kind: HomeActionKind::DisconnectSigner, - label: "Disconnect Remote Signer".to_owned(), - enabled: true, - pending: false, - }] - } else { - Vec::new() - } - } - RadrootsNostrSelectedAccountStatus::Ready { .. } => vec![ - HomeActionState { - kind: HomeActionKind::BackupSecretKey, - label: "Back Up Secret Key".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::RevealRawSecretKey, - label: "Reveal Raw Secret Key".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::RemoveLocalKey, - label: "Remove Key From This Device".to_owned(), - enabled: true, - pending: false, - }, - HomeActionState { - kind: HomeActionKind::ResetDevice, - label: "Reset This Device".to_owned(), - enabled: true, - pending: false, - }, - ], - } - } - - fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> { - let manager = Self::accounts_manager()?; - match action { - HomeActionKind::BackupSecretKey => Ok(HomeActionResult::None), - HomeActionKind::RevealRawSecretKey => { - Self::export_selected_local_raw_secret_key(&manager) - .map(|nsec| HomeActionResult::RevealRawSecretKey { nsec }) - } - HomeActionKind::RemoveLocalKey => { - Self::remove_selected_local_identity(&manager).map(HomeActionResult::IdentityState) - } - HomeActionKind::ResetDevice => { - let accounts_path = storage::accounts_path()?; - Self::reset_local_device_state(&manager, accounts_path.as_path()) - .map(HomeActionResult::IdentityState) - } - HomeActionKind::DisconnectSigner => { - remote_signer::disconnect_selected_remote_signer(&manager) - .map(HomeActionResult::IdentityState) - } - } - } - - fn request_secret_key_backup_action(&self, password: &str) -> Result<HomeActionResult, String> { - let manager = Self::accounts_manager()?; - Self::export_selected_local_encrypted_secret_key(&manager, password) - .map(|ncryptsec| HomeActionResult::RevealEncryptedSecretKey { ncryptsec }) - } - - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - self.remote_signer - .take_update() - .transpose() - .map(|state| state.flatten()) - } -} - -#[cfg(target_os = "ios")] -fn native_options() -> eframe::NativeOptions { - eframe::NativeOptions { - renderer: eframe::Renderer::Wgpu, - viewport: ViewportBuilder::default() - .with_title(APP_NAME) - .with_fullscreen(true), - ..Default::default() - } -} - -#[cfg(target_os = "ios")] -pub fn run() -> Result<(), String> { - eframe::run_native( - APP_NAME, - native_options(), - Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(IosBackend::new()))))), - ) - .map_err(|err| err.to_string()) -} - -#[cfg(not(target_os = "ios"))] -pub fn run() -> Result<(), String> { - Err("radroots_app_ios can only launch on an ios target".to_owned()) -} - -pub const ENTRYPOINT_SYMBOL: &str = "radroots_ios_run"; - -#[allow(unsafe_code)] -#[unsafe(no_mangle)] -pub extern "C" fn radroots_ios_run() -> i32 { - match run() { - Ok(()) => 0, - Err(_) => 1, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{ - FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, fixture_identity_ncryptsec, - }; - use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; - - #[test] - fn non_ios_run_is_rejected() { - #[cfg(not(target_os = "ios"))] - assert_eq!( - run(), - Err("radroots_app_ios can only launch on an ios target".to_owned()) - ); - } - - #[test] - fn exported_entrypoint_symbol_is_stable() { - assert_eq!(ENTRYPOINT_SYMBOL, "radroots_ios_run"); - } - - #[test] - fn new_ios_manager_starts_in_setup_state() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - assert_eq!( - IosBackend::identity_state_from_manager(&manager), - Ok(IdentityGateState::Missing) - ); - } - - #[test] - fn local_identity_generation_transitions_to_ready() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - let state = IosBackend::generate_local_identity(&manager).expect("generate identity"); - let IdentityGateState::Ready { account_id } = state else { - panic!("expected ready identity state"); - }; - - assert!(!account_id.is_empty()); - } - - #[test] - fn local_identity_removal_transitions_back_to_missing() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - IosBackend::generate_local_identity(&manager).expect("generate identity"); - let state = - IosBackend::remove_selected_local_identity(&manager).expect("remove selected account"); - - assert_eq!(state, IdentityGateState::Missing); - assert_eq!( - manager.selected_account_id().expect("selected account"), - None - ); - } - - #[test] - fn remove_all_local_identities_clears_every_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - - manager - .generate_identity(Some("first".into()), true) - .expect("generate first"); - manager - .generate_identity(Some("second".into()), false) - .expect("generate second"); - - let state = IosBackend::remove_all_local_identities(&manager).expect("reset state"); - - assert_eq!(state, IdentityGateState::Missing); - assert_eq!(manager.list_accounts().expect("list accounts").len(), 0); - assert_eq!(manager.selected_account_id().expect("selected"), None); - } - - #[test] - fn export_selected_local_raw_secret_key_returns_nsec() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - manager - .upsert_identity(&identity, Some("primary".into()), true) - .expect("store identity"); - - let nsec = - IosBackend::export_selected_local_raw_secret_key(&manager).expect("export secret"); - - assert_eq!(nsec, identity.nsec()); - assert!(nsec.starts_with("nsec1")); - } - - #[test] - fn export_selected_local_encrypted_secret_key_returns_ncryptsec() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - - manager - .upsert_identity(&fixture_identity, Some("primary".into()), true) - .expect("store identity"); - - let ncryptsec = IosBackend::export_selected_local_encrypted_secret_key( - &manager, - FIXTURE_BACKUP_PASSWORD, - ) - .expect("export encrypted secret"); - - let restored = RadrootsIdentity::from_encrypted_secret_key_str( - ncryptsec.as_str(), - FIXTURE_BACKUP_PASSWORD, - ) - .expect("restore encrypted secret"); - - assert_eq!(restored.secret_key_hex(), FIXTURE_ALICE.secret_key_hex); - } - - #[test] - fn import_local_identity_imports_raw_secret_key_and_selects_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - let state = IosBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::RawSecretKey, - secret_text: identity.nsec(), - password: None, - }, - ) - .expect("import"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: identity.id().to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(identity.id()) - ); - assert_eq!(manager.list_accounts().expect("list").len(), 1); - assert_eq!( - manager - .export_secret_hex(&identity.id()) - .expect("export secret"), - Some(identity.secret_key_hex()) - ); - } - - #[test] - fn import_local_identity_imports_encrypted_secret_key_and_selects_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let fixture_identity = - RadrootsIdentity::from_secret_key_str(FIXTURE_ALICE.secret_key_hex).expect("fixture"); - let fixture_account_id = fixture_identity.id(); - - let state = IosBackend::import_local_identity( - &manager, - &RadrootsSecretImportRequest { - mode: RadrootsSecretImportMode::EncryptedSecretKey, - secret_text: encrypted_secret_key, - password: Some(FIXTURE_BACKUP_PASSWORD.to_owned()), - }, - ) - .expect("import"); - - assert_eq!( - state, - IdentityGateState::Ready { - account_id: fixture_account_id.to_string(), - } - ); - assert_eq!( - manager.selected_account_id().expect("selected"), - Some(fixture_account_id) - ); - assert_eq!(manager.list_accounts().expect("list").len(), 1); - assert_eq!( - manager - .export_secret_hex(&fixture_identity.id()) - .expect("export secret"), - Some(FIXTURE_ALICE.secret_key_hex.to_owned()) - ); - } - - #[test] - fn account_roster_from_manager_lists_local_managed_account() { - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let identity = RadrootsIdentity::generate(); - - manager - .upsert_identity(&identity, Some("primary".into()), true) - .expect("store identity"); - - let roster = IosBackend::account_roster_from_manager(&manager).expect("account roster"); - - assert_eq!(roster.len(), 1); - assert_eq!(roster[0].account_id, identity.id().to_string()); - assert_eq!(roster[0].npub, identity.npub()); - assert_eq!(roster[0].label.as_deref(), Some("primary")); - assert_eq!(roster[0].custody, RadrootsAccountCustody::LocalManaged); - } - - #[test] - fn normalize_clipboard_secret_key_text_trims_wrapping_whitespace() { - let clipboard_text = format!(" {} \n", FIXTURE_ALICE.nsec); - let normalized = IosBackend::normalize_clipboard_secret_key_text(clipboard_text.as_str()) - .expect("normalize secret key"); - - assert_eq!(normalized, FIXTURE_ALICE.nsec); - } - - #[test] - fn normalize_clipboard_secret_key_text_rejects_blank_text() { - assert_eq!( - IosBackend::normalize_clipboard_secret_key_text(" \n\t"), - Err("clipboard does not contain text".to_owned()) - ); - } - - #[test] - fn remove_accounts_file_if_present_deletes_existing_file() { - let unique = format!( - "radroots-ios-reset-{}-{}.json", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time") - .as_nanos() - ); - let path = std::env::temp_dir().join(unique); - std::fs::write(&path, b"{}").expect("write accounts file"); - - IosBackend::remove_accounts_file_if_present(path.as_path()).expect("remove file"); - - assert!(!path.exists()); - } -} diff --git a/crates/launchers/ios/src/offline_geocoder.rs b/crates/launchers/ios/src/offline_geocoder.rs @@ -1,516 +0,0 @@ -#![cfg_attr(not(target_os = "ios"), allow(dead_code))] - -use radroots_app_core::{ - RadrootsLocationCountry, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation, -}; -use radroots_geocoder::{ - Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, - GeocoderReverseResult, -}; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -const GEOCODER_ASSET_FILENAME: &str = "geonames.db"; -const GEOCODER_REVISION_FILENAME: &str = "geonames.revision"; - -#[derive(Clone)] -pub(crate) struct IosOfflineGeocoder { - current: Arc<Mutex<RadrootsOfflineGeocoderState>>, - changed: Arc<AtomicBool>, -} - -impl IosOfflineGeocoder { - pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self { - Self { - current: Arc::new(Mutex::new(state)), - changed: Arc::new(AtomicBool::new(false)), - } - } - - pub(crate) fn start(app_data_root: PathBuf) -> Self { - let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing); - let current = Arc::clone(&tracker.current); - let changed = Arc::clone(&tracker.changed); - - std::thread::spawn(move || { - let state = initialize_offline_geocoder(app_data_root.as_path()); - if let RadrootsOfflineGeocoderState::Unavailable { debug_message, .. } = &state { - log::warn!("ios offline geocoder unavailable: {debug_message}"); - } - if let Ok(mut slot) = current.lock() { - *slot = state; - changed.store(true, Ordering::Release); - } - }); - - tracker - } - - pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState { - self.current - .lock() - .map(|state| state.clone()) - .unwrap_or_else(|_| { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Ios, - "ios offline geocoder state lock poisoned", - ) - }) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> { - if self.changed.swap(false, Ordering::AcqRel) { - Some(self.current_state()) - } else { - None - } - } -} - -pub(crate) fn reverse_location( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, -) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - let options = options.map(|options| GeocoderReverseOptions { - limit: options.limit, - degree_offset: options.degree_offset, - }); - geocoder - .reverse( - GeocoderPoint { - lat: point.lat, - lng: point.lng, - }, - options, - ) - .map(|results| results.into_iter().map(map_reverse_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -pub(crate) fn list_countries( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, -) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - geocoder - .country_list() - .map(|results| results.into_iter().map(map_country_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) -} - -pub(crate) fn country_center( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, - country_id: &str, -) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - let geocoder = geocoder_for_queries(app_data_root, state)?; - geocoder - .country_center(country_id) - .map(|point| RadrootsLocationPoint { - lat: point.lat, - lng: point.lng, - }) - .map_err(map_country_center_error) -} - -fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { - let source_path = bundled_asset_path().map_err(|debug_message| { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InternalError, - RadrootsOfflineGeocoderPlatform::Ios, - debug_message, - ) - }); - let source_path = match source_path { - Ok(source_path) => source_path, - Err(state) => return state, - }; - if !source_path.is_file() { - return RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Ios, - format!( - "ios bundled geocoder asset missing at {}", - source_path.display() - ), - ); - } - - let revision = - match bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) { - Ok(revision) => revision, - Err((kind, debug_message)) => { - return RadrootsOfflineGeocoderState::unavailable( - kind, - RadrootsOfflineGeocoderPlatform::Ios, - debug_message, - ); - } - }; - let staged_path = staged_db_path(app_data_root, revision.as_str()); - if let Err(debug_message) = stage_bundled_asset(source_path.as_path(), staged_path.as_path()) { - return RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Ios, - revision, - debug_message, - ); - } - if let Err(source) = Geocoder::open_path(staged_path.as_path()) { - return RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Ios, - revision, - format!("failed to open staged ios geocoder db: {source}"), - ); - } - let _ = prune_stale_revisions(staged_geocoder_root(app_data_root), revision.as_str()); - RadrootsOfflineGeocoderState::Ready -} - -fn bundled_asset_path() -> Result<PathBuf, String> { - let executable_path = std::env::current_exe() - .map_err(|source| format!("failed to resolve ios executable path: {source}"))?; - let Some(parent) = executable_path.parent() else { - return Err("ios executable path did not have a parent directory".to_owned()); - }; - Ok(parent.join(GEOCODER_ASSET_FILENAME)) -} - -fn geocoder_for_queries( - app_data_root: &Path, - state: &RadrootsOfflineGeocoderState, -) -> Result<Geocoder, RadrootsLocationResolverError> { - match state { - RadrootsOfflineGeocoderState::Initializing => { - return Err(RadrootsLocationResolverError::Initializing); - } - RadrootsOfflineGeocoderState::Unavailable { .. } => { - return Err(RadrootsLocationResolverError::Unavailable); - } - RadrootsOfflineGeocoderState::Ready => {} - } - - let source_path = bundled_asset_path() - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - let revision = - bundled_asset_revision(source_path.parent().unwrap_or_else(|| Path::new("."))) - .map_err(|(_, message)| RadrootsLocationResolverError::QueryFailed { message })?; - let staged_path = staged_db_path(app_data_root, revision.as_str()); - stage_bundled_asset(source_path.as_path(), staged_path.as_path()) - .map_err(|message| RadrootsLocationResolverError::QueryFailed { message })?; - Geocoder::open_path(staged_path.as_path()).map_err(|source| { - RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - } - }) -} - -fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation { - RadrootsResolvedLocation { - id: result.id, - name: result.name, - admin1_id: result.admin1_id, - admin1_name: result.admin1_name, - country_id: result.country_id, - country_name: result.country_name, - point: RadrootsLocationPoint { - lat: result.latitude, - lng: result.longitude, - }, - } -} - -fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry { - RadrootsLocationCountry { - country_id: result.country_id, - country_name: result.country, - center: RadrootsLocationPoint { - lat: result.lat, - lng: result.lng, - }, - } -} - -fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError { - match source { - GeocoderError::CountryCenterNotFound { country_id } => { - RadrootsLocationResolverError::CountryCenterNotFound { country_id } - } - other => RadrootsLocationResolverError::QueryFailed { - message: other.to_string(), - }, - } -} - -fn bundled_asset_revision( - asset_dir: &Path, -) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> { - let revision_path = asset_dir.join(GEOCODER_REVISION_FILENAME); - let revision = std::fs::read_to_string(revision_path.as_path()).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - format!( - "ios bundled geocoder revision asset missing at {}: {source}", - revision_path.display() - ), - ) - })?; - let revision = revision.trim(); - if !is_valid_revision(revision) { - return Err(( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - format!( - "ios bundled geocoder revision asset invalid at {}", - revision_path.display() - ), - )); - } - Ok(revision.to_owned()) -} - -fn is_valid_revision(revision: &str) -> bool { - revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()) -} - -fn staged_geocoder_root(app_data_root: &Path) -> PathBuf { - app_data_root.join("geocoder") -} - -fn staged_db_path(app_data_root: &Path, revision: &str) -> PathBuf { - staged_geocoder_root(app_data_root) - .join(revision) - .join(GEOCODER_ASSET_FILENAME) -} - -fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> { - let Some(parent) = staged_path.parent() else { - return Err("staged ios geocoder path did not have a parent directory".to_owned()); - }; - std::fs::create_dir_all(parent) - .map_err(|source| format!("failed to create ios geocoder directory: {source}"))?; - if staged_path.is_file() { - return Ok(false); - } - std::fs::copy(source_path, staged_path) - .map_err(|source| format!("failed to stage ios geocoder asset: {source}"))?; - Ok(true) -} - -fn prune_stale_revisions(staged_root: PathBuf, active_revision: &str) -> Result<(), String> { - if !staged_root.is_dir() { - return Ok(()); - } - - for entry in std::fs::read_dir(staged_root.as_path()) - .map_err(|source| format!("failed to list ios geocoder revisions: {source}"))? - { - let entry = entry - .map_err(|source| format!("failed to read ios geocoder revision entry: {source}"))?; - if entry.file_name() == std::ffi::OsStr::new(active_revision) { - continue; - } - - let path = entry.path(); - if entry - .file_type() - .map_err(|source| format!("failed to inspect ios geocoder revision entry: {source}"))? - .is_dir() - { - std::fs::remove_dir_all(path.as_path()).map_err(|source| { - format!( - "failed to remove stale ios geocoder revision {}: {source}", - path.display() - ) - })?; - } else { - std::fs::remove_file(path.as_path()).map_err(|source| { - format!( - "failed to remove stale ios geocoder revision file {}: {source}", - path.display() - ) - })?; - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[test] - fn staged_db_path_uses_ios_geocoder_directory() { - let app_data_root = PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app", - ); - - assert_eq!( - staged_db_path(app_data_root.as_path(), "abcd"), - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app/geocoder/abcd/geonames.db" - ) - ); - } - - #[test] - fn valid_revision_requires_sha256_hex() { - assert!(is_valid_revision( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" - )); - assert!(!is_valid_revision("abcd")); - assert!(!is_valid_revision( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079z" - )); - } - - #[test] - fn missing_asset_maps_to_build_unavailable_message() { - let state = RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Ios, - "ios bundled geocoder asset missing at /tmp/geonames.db", - ); - - assert_eq!( - state, - RadrootsOfflineGeocoderState::Unavailable { - kind: RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - platform: RadrootsOfflineGeocoderPlatform::Ios, - asset_revision: None, - debug_message: "ios bundled geocoder asset missing at /tmp/geonames.db".to_owned(), - } - ); - } - - #[test] - fn stage_bundled_asset_reuses_existing_staged_copy() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-ios-geocoder-test-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let source_path = temp_root.join("source.db"); - let staged_path = temp_root.join("staged").join("geonames.db"); - - std::fs::create_dir_all(temp_root.as_path()).unwrap(); - std::fs::write(source_path.as_path(), b"source").unwrap(); - std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap(); - std::fs::write(staged_path.as_path(), b"existing").unwrap(); - - let copied = stage_bundled_asset(source_path.as_path(), staged_path.as_path()).unwrap(); - - assert!(!copied); - assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing"); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn prune_stale_revisions_keeps_active_revision_only() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-ios-geocoder-prune-test-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let staged_root = temp_root.join("geocoder"); - let active_dir = staged_root.join("active"); - let stale_dir = staged_root.join("stale"); - let stale_file = staged_root.join("orphan.txt"); - - std::fs::create_dir_all(active_dir.as_path()).unwrap(); - std::fs::create_dir_all(stale_dir.as_path()).unwrap(); - std::fs::write(active_dir.join("geonames.db"), b"active").unwrap(); - std::fs::write(stale_dir.join("geonames.db"), b"stale").unwrap(); - std::fs::write(stale_file.as_path(), b"orphan").unwrap(); - - prune_stale_revisions(staged_root.clone(), "active").unwrap(); - - assert!(active_dir.exists()); - assert!(!stale_dir.exists()); - assert!(!stale_file.exists()); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn bundled_asset_revision_reads_stamped_sidecar() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-ios-geocoder-revision-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - let revision_path = temp_root.join(GEOCODER_REVISION_FILENAME); - let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"; - - std::fs::create_dir_all(temp_root.as_path()).unwrap(); - std::fs::write(revision_path.as_path(), format!("{revision}\n")).unwrap(); - - assert_eq!( - bundled_asset_revision(temp_root.as_path()).unwrap(), - revision.to_owned() - ); - - std::fs::remove_dir_all(temp_root.as_path()).unwrap(); - } - - #[test] - fn reverse_result_mapping_preserves_location_fields() { - let mapped = map_reverse_result(GeocoderReverseResult { - id: 42, - name: "Oslo".to_owned(), - admin1_id: Some(12), - admin1_name: Some("Oslo".to_owned()), - country_id: "NO".to_owned(), - country_name: Some("Norway".to_owned()), - latitude: 59.9139, - longitude: 10.7522, - }); - - assert_eq!(mapped.id, 42); - assert_eq!(mapped.name, "Oslo"); - assert_eq!(mapped.admin1_id, Some(12)); - assert_eq!(mapped.admin1_name.as_deref(), Some("Oslo")); - assert_eq!(mapped.country_id, "NO"); - assert_eq!(mapped.country_name.as_deref(), Some("Norway")); - assert_eq!(mapped.point.lat, 59.9139); - assert_eq!(mapped.point.lng, 10.7522); - } - - #[test] - fn unavailable_state_blocks_queries_until_ready() { - let temp_root = std::env::temp_dir().join(format!( - "radroots-ios-geocoder-query-state-test-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() - )); - - let result = list_countries( - temp_root.as_path(), - &RadrootsOfflineGeocoderState::Initializing, - ); - - assert_eq!(result, Err(RadrootsLocationResolverError::Initializing)); - } -} diff --git a/crates/launchers/ios/src/remote_signer.rs b/crates/launchers/ios/src/remote_signer.rs @@ -1,469 +0,0 @@ -use crate::storage; -use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault}; -use radroots_app_core::{ - IdentityGateState, RadrootsAccountCustody, RadrootsPendingRemoteSignerConnection, - RadrootsRemoteSignerPreview, RadrootsRemoteSignerSignedNote, SetupActionState, -}; -use radroots_app_remote_signer::{ - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerActionController, - RadrootsAppRemoteSignerActionControllerHooks, RadrootsAppRemoteSignerActionState, - RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, - RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerPendingState, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_clear_pending_session, - radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, - radroots_app_remote_signer_purge_all_custody_state, - radroots_app_remote_signer_reconcile_startup, -}; -use radroots_identity::RadrootsIdentityId; -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, RadrootsSecretVault, - account_secret_slot, -}; -use std::path::{Path, PathBuf}; - -const REMOTE_SIGNER_LABEL: &str = "remote signer"; - -#[derive(Clone, Copy)] -struct IosRemoteSignerHooks; - -impl RadrootsAppRemoteSignerControllerHooks for IosRemoteSignerHooks { - type ReadyState = IdentityGateState; - - fn reconcile_startup_state(&self) -> Result<(), String> { - let manager = storage::accounts_manager()?; - let store_path = sessions_path()?; - radroots_app_remote_signer_reconcile_startup( - &manager, - store_path.as_path(), - REMOTE_SIGNER_LABEL, - load_client_secret, - remove_client_secret, - purge_client_secret_namespace, - ) - } - - fn store_pending_session( - &self, - pending: &RadrootsAppRemoteSignerPendingSession, - ) -> Result<(), String> { - let client_account_id = pending.record.client_account_id().to_owned(); - store_client_secret( - client_account_id.as_str(), - pending.client_secret_key_hex.as_str(), - )?; - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - if let Err(error) = state.upsert_pending(pending.record.clone()) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error.to_string()); - } - if let Err(error) = save_sessions(store_path.as_path(), &state) { - let _ = remove_client_secret(client_account_id.as_str()); - return Err(error); - } - Ok(()) - } - - fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - pending_session_record() - } - - fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> { - load_client_secret(client_account_id) - } - - fn activate_pending_session( - &self, - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, - ) -> Result<Self::ReadyState, String> { - activate_remote_session(client_account_id, approved) - } - - fn clear_pending_session( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_clear_pending_session(store_path.as_path(), remove_client_secret) - } -} - -#[derive(Clone)] -pub(crate) struct IosRemoteSigner { - controller: RadrootsAppRemoteSignerController<IosRemoteSignerHooks>, - action_controller: RadrootsAppRemoteSignerActionController<IosRemoteSignerHooks>, -} - -impl IosRemoteSigner { - pub(crate) fn new() -> Self { - Self { - controller: RadrootsAppRemoteSignerController::new(IosRemoteSignerHooks), - action_controller: RadrootsAppRemoteSignerActionController::new(IosRemoteSignerHooks), - } - } - - pub(crate) fn take_update(&self) -> Option<Result<Option<IdentityGateState>, String>> { - self.controller.take_update() - } - - pub(crate) fn is_connecting(&self) -> bool { - self.controller.is_connecting() - } - - pub(crate) fn action_state(&self) -> Result<SetupActionState, String> { - if self.is_connecting() { - return Ok(SetupActionState { - label: "Connecting Remote Signer...".to_owned(), - enabled: false, - pending: true, - }); - } - - if self.pending_connection()?.is_some() { - return Ok(match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { - label: "Remote Signer Approval Check Retrying".to_owned(), - enabled: false, - pending: false, - }, - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { .. } => { - SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - } - } - RadrootsAppRemoteSignerPendingState::Idle - | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, - }, - }); - } - - Ok(SetupActionState { - label: "Connect Remote Signer".to_owned(), - enabled: true, - pending: false, - }) - } - - pub(crate) fn begin_connect(&self, input: &str) -> Result<(), String> { - self.controller.begin_connect(input) - } - - pub(crate) fn pending_connection( - &self, - ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> { - Ok( - pending_session_record()?.map(|record| RadrootsPendingRemoteSignerConnection { - signer_npub: record.signer_identity.public_key_npub, - relays: record.relays, - auth_url: match self.controller.pending_state() { - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url } => Some(url), - _ => None, - }, - }), - ) - } - - pub(crate) fn note_action_state(&self) -> Result<SetupActionState, String> { - if selected_remote_signer_account()?.is_none() { - return Ok(SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: false, - pending: false, - }); - } - - Ok(match self.action_controller.state() { - RadrootsAppRemoteSignerActionState::Idle => SetupActionState { - label: "Sign Remote Kind 1 Note".to_owned(), - enabled: true, - pending: false, - }, - RadrootsAppRemoteSignerActionState::Signing => SetupActionState { - label: "Signing Remote Kind 1 Note...".to_owned(), - enabled: false, - pending: true, - }, - RadrootsAppRemoteSignerActionState::AwaitingAuthorization { .. } => SetupActionState { - label: "Authorize Remote Signer to Continue".to_owned(), - enabled: false, - pending: false, - }, - }) - } - - pub(crate) fn begin_sign_kind1_note_selected(&self, content: &str) -> Result<(), String> { - self.action_controller.begin_sign_kind1_note(content) - } - - pub(crate) fn take_note_update( - &self, - ) -> Option<Result<Option<RadrootsRemoteSignerSignedNote>, String>> { - self.action_controller.take_update() - } -} - -pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { - let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; - let requested_permissions = preview.requested_permission_labels(); - Ok(RadrootsRemoteSignerPreview { - source_label: preview.source_label().to_owned(), - signer_npub: preview.signer_identity.public_key_npub, - relays: preview.relays, - requested_permissions, - }) -} - -pub(crate) fn identity_state_from_status( - status: RadrootsNostrSelectedAccountStatus, -) -> Result<IdentityGateState, String> { - match status { - RadrootsNostrSelectedAccountStatus::NotConfigured => Ok(IdentityGateState::Missing), - RadrootsNostrSelectedAccountStatus::Ready { account } => Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }), - RadrootsNostrSelectedAccountStatus::PublicOnly { account } => { - if active_session_for_account_id(account.account_id.as_str())?.is_some() { - Ok(IdentityGateState::Ready { - account_id: account.account_id.to_string(), - }) - } else { - Ok(IdentityGateState::Missing) - } - } - } -} - -pub(crate) fn custody_for_account_id(account_id: &str) -> Result<RadrootsAccountCustody, String> { - if active_session_for_account_id(account_id)?.is_some() { - Ok(RadrootsAccountCustody::RemoteSigner) - } else { - Ok(RadrootsAccountCustody::LocalManaged) - } -} - -pub(crate) fn disconnect_selected_remote_signer( - manager: &RadrootsNostrAccountsManager, -) -> Result<IdentityGateState, String> { - let store_path = sessions_path()?; - let status = radroots_app_remote_signer_disconnect_selected( - manager, - store_path.as_path(), - remove_client_secret, - )?; - identity_state_from_status(status) -} - -pub(crate) fn cancel_pending_connection() -> Result<(), String> { - let store_path = sessions_path()?; - let _ = radroots_app_remote_signer_clear_pending_session( - store_path.as_path(), - remove_client_secret, - )?; - Ok(()) -} - -pub(crate) fn purge_all_custody_state() -> Result<(), String> { - let store_path = sessions_path()?; - radroots_app_remote_signer_purge_all_custody_state( - store_path.as_path(), - remove_client_secret, - purge_client_secret_namespace, - ) -} - -fn activate_remote_session( - client_account_id: &str, - approved: radroots_app_remote_signer::RadrootsAppRemoteSignerApprovedSession, -) -> Result<IdentityGateState, String> { - let manager = storage::accounts_manager()?; - manager - .upsert_public_identity( - approved.user_identity.clone(), - Some(REMOTE_SIGNER_LABEL.to_owned()), - true, - ) - .map_err(|source| source.to_string())?; - let store_path = sessions_path()?; - let activation_result = (|| -> Result<(), String> { - let mut state = load_sessions(store_path.as_path())?; - state - .activate_session( - client_account_id, - approved.user_identity.clone(), - approved.relays.clone(), - approved.approved_permissions.clone(), - ) - .ok_or_else(|| { - "pending remote signer session disappeared before activation".to_owned() - })?; - save_sessions(store_path.as_path(), &state) - })(); - if let Err(error) = activation_result { - if let Err(rollback_error) = manager.remove_account(&approved.user_identity.id) { - return Err(format!( - "{error}. remote signer account rollback needs retry: {rollback_error}" - )); - } - return Err(error); - } - Ok(IdentityGateState::Ready { - account_id: approved.user_identity.id.to_string(), - }) -} - -fn selected_remote_signer_account() -> Result<Option<String>, String> { - let manager = storage::accounts_manager()?; - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(None); - }; - if active_session_for_account_id(account_id.as_str())?.is_some() { - Ok(Some(account_id.to_string())) - } else { - Ok(None) - } -} - -fn update_active_session_relays(account_id: &str, relays: Vec<String>) -> Result<(), String> { - let store_path = sessions_path()?; - let mut state = load_sessions(store_path.as_path())?; - let Some(mut session) = state.active_session_for_account_id(account_id).cloned() else { - return Err("active remote signer session disappeared before relay update".to_owned()); - }; - if session.relays == relays { - return Ok(()); - } - session.relays = relays; - state.remove_active_session_for_account_id(account_id); - state.sessions.push(session); - save_sessions(store_path.as_path(), &state) -} - -impl RadrootsAppRemoteSignerActionControllerHooks for IosRemoteSignerHooks { - type ReadyState = RadrootsRemoteSignerSignedNote; - - fn selected_active_session( - &self, - ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Ok(None); - }; - let Some(record) = active_session_for_account_id(account_id.as_str())? else { - return Ok(None); - }; - let secret = load_client_secret(record.client_account_id())?; - Ok(Some((record, secret))) - } - - fn complete_sign_event( - &self, - signed_event: RadrootsAppRemoteSignerSignedEvent, - ) -> Result<Self::ReadyState, String> { - let Some(account_id) = selected_remote_signer_account()? else { - return Err("remote signer account is no longer selected".to_owned()); - }; - update_active_session_relays(account_id.as_str(), signed_event.relays.clone())?; - Ok(RadrootsRemoteSignerSignedNote { - event_id_hex: signed_event.event_id_hex, - }) - } -} - -fn pending_session_record() -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.pending_session().cloned()) -} - -fn active_session_for_account_id( - account_id: &str, -) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let store_path = sessions_path()?; - let state = load_sessions(store_path.as_path())?; - Ok(state.active_session_for_account_id(account_id).cloned()) -} - -fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> { - RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string()) -} - -fn save_sessions( - path: &Path, - state: &RadrootsAppRemoteSignerSessionStoreState, -) -> Result<(), String> { - state.save(path).map_err(|error| error.to_string()) -} - -fn sessions_path() -> Result<PathBuf, String> { - Ok(storage::app_data_root()? - .join("nostr") - .join("remote-signer-sessions.json")) -} - -fn client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_with_namespace_device_local( - APPLE_NOSTR_SERVICE, - RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, - ) -} - -fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_device_local(APPLE_NOSTR_SERVICE) -} - -fn client_secret_slot(client_account_id: &str) -> Result<String, String> { - let account_id = RadrootsIdentityId::try_from(client_account_id) - .map_err(|_| "invalid remote signer client account id".to_owned())?; - Ok(account_secret_slot(&account_id)) -} - -fn store_client_secret(client_account_id: &str, secret_key_hex: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .store_secret(slot.as_str(), secret_key_hex) - .map_err(|source| source.to_string()) -} - -fn load_client_secret(client_account_id: &str) -> Result<String, String> { - let slot = client_secret_slot(client_account_id)?; - if let Some(secret) = client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - { - return Ok(secret); - } - - let secret = legacy_client_secret_vault() - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - .ok_or_else(|| "remote signer session secret is missing".to_owned())?; - let _ = client_secret_vault().store_secret(slot.as_str(), secret.as_str()); - let _ = legacy_client_secret_vault().remove_secret(slot.as_str()); - Ok(secret) -} - -fn remove_client_secret(client_account_id: &str) -> Result<(), String> { - let slot = client_secret_slot(client_account_id)?; - client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string())?; - legacy_client_secret_vault() - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string()) -} - -fn purge_client_secret_namespace() -> Result<(), String> { - client_secret_vault() - .purge_namespace() - .map_err(|source| source.to_string()) -} diff --git a/crates/launchers/ios/src/reverse_lookup.rs b/crates/launchers/ios/src/reverse_lookup.rs @@ -1,122 +0,0 @@ -#![cfg_attr(not(target_os = "ios"), allow(dead_code))] - -#[cfg(target_os = "ios")] -use crate::offline_geocoder; -use radroots_app_core::{ - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderState, RadrootsReverseLocationLookupResult, -}; -#[cfg(target_os = "ios")] -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Clone, Default)] -pub(crate) struct IosReverseLookup { - result: Arc<Mutex<Option<RadrootsReverseLocationLookupResult>>>, - changed: Arc<AtomicBool>, - pending: Arc<AtomicBool>, -} - -impl IosReverseLookup { - pub(crate) fn new() -> Self { - Self::default() - } - - #[cfg(target_os = "ios")] - pub(crate) fn begin( - &self, - app_data_root: PathBuf, - geocoder_state: RadrootsOfflineGeocoderState, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - if self.pending.swap(true, Ordering::AcqRel) { - return Err(RadrootsLocationResolverError::QueryFailed { - message: "offline location query is already running".to_owned(), - }); - } - - if let Ok(mut slot) = self.result.lock() { - *slot = None; - } - - let result = Arc::clone(&self.result); - let changed = Arc::clone(&self.changed); - let pending = Arc::clone(&self.pending); - std::thread::spawn(move || { - let lookup_result = offline_geocoder::reverse_location( - app_data_root.as_path(), - &geocoder_state, - point, - options, - ); - if let Ok(mut slot) = result.lock() { - *slot = Some(lookup_result); - changed.store(true, Ordering::Release); - } - pending.store(false, Ordering::Release); - }); - - Ok(()) - } - - #[cfg(not(target_os = "ios"))] - pub(crate) fn begin( - &self, - _app_data_root: std::path::PathBuf, - _geocoder_state: RadrootsOfflineGeocoderState, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - pub(crate) fn take_update(&self) -> Option<RadrootsReverseLocationLookupResult> { - if !self.changed.swap(false, Ordering::AcqRel) { - return None; - } - - match self.result.lock() { - Ok(mut slot) => slot.take(), - Err(_) => Some(Err(RadrootsLocationResolverError::QueryFailed { - message: "ios reverse lookup result lock poisoned".to_owned(), - })), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_core::RadrootsResolvedLocation; - - fn sample_result() -> RadrootsReverseLocationLookupResult { - Ok(vec![RadrootsResolvedLocation { - id: 7, - name: "example".to_owned(), - admin1_id: None, - admin1_name: None, - country_id: "US".to_owned(), - country_name: Some("United States".to_owned()), - point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 }, - }]) - } - - #[test] - fn take_update_is_none_until_tracker_changes() { - let tracker = IosReverseLookup::new(); - - assert_eq!(tracker.take_update(), None); - } - - #[test] - fn take_update_returns_queued_result_once() { - let tracker = IosReverseLookup::new(); - *tracker.result.lock().unwrap() = Some(sample_result()); - tracker.changed.store(true, Ordering::Release); - - assert!(matches!(tracker.take_update(), Some(Ok(results)) if results.len() == 1)); - assert_eq!(tracker.take_update(), None); - } -} diff --git a/crates/launchers/ios/src/storage.rs b/crates/launchers/ios/src/storage.rs @@ -1,124 +0,0 @@ -#[cfg(target_os = "ios")] -use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault}; -use radroots_app_core::mobile_native_app_storage_layout; -#[cfg(target_os = "ios")] -use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; -use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform}; -use std::path::{Path, PathBuf}; - -fn mobile_base_root_from_home(home: &Path) -> PathBuf { - home.join("Library") - .join("Application Support") - .join("RadRoots") -} - -fn app_paths_from_home(home: &Path) -> Result<RadrootsPaths, String> { - let base_root = mobile_base_root_from_home(home); - Ok(mobile_native_app_storage_layout(RadrootsPlatform::Ios, base_root.as_path())?.app_paths) -} - -#[cfg(target_os = "ios")] -pub(crate) fn accounts_path() -> Result<PathBuf, String> { - let home = std::env::var_os("HOME") - .map(PathBuf::from) - .ok_or_else(|| "failed to resolve ios app container home directory".to_owned())?; - let accounts_path = accounts_path_from_home(home.as_path())?; - if let Some(parent) = accounts_path.parent() { - ensure_private_directory_tree(parent)?; - } - Ok(accounts_path) -} - -#[cfg(target_os = "ios")] -pub(crate) fn app_data_root() -> Result<PathBuf, String> { - let home = std::env::var_os("HOME") - .map(PathBuf::from) - .ok_or_else(|| "failed to resolve ios app container home directory".to_owned())?; - let root = app_data_root_from_home(home.as_path())?; - ensure_private_directory_tree(root.as_path())?; - Ok(root) -} - -#[cfg(target_os = "ios")] -pub(crate) fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { - RadrootsNostrAccountsManager::new_file_backed_with_vault( - accounts_path()?, - RadrootsAppleKeychainVault::new_device_local(APPLE_NOSTR_SERVICE), - ) - .map_err(|source| source.to_string()) -} - -fn accounts_path_from_home(home: &Path) -> Result<PathBuf, String> { - Ok(app_data_root_from_home(home)? - .join("nostr") - .join("accounts.json")) -} - -fn app_data_root_from_home(home: &Path) -> Result<PathBuf, String> { - Ok(app_paths_from_home(home)?.data) -} - -#[cfg(target_os = "ios")] -fn ensure_private_directory_tree(leaf: &Path) -> Result<(), String> { - use std::os::unix::fs::PermissionsExt; - - std::fs::create_dir_all(leaf) - .map_err(|source| format!("failed to create ios app data directory: {source}"))?; - std::fs::set_permissions(leaf, std::fs::Permissions::from_mode(0o700)) - .map_err(|source| format!("failed to set ios app data permissions: {source}"))?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn accounts_path_uses_ios_mobile_native_layout() { - let home = PathBuf::from("/var/mobile/Containers/Data/Application/example"); - - assert_eq!( - accounts_path_from_home(home.as_path()).expect("accounts path"), - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app/nostr/accounts.json" - ) - ); - } - - #[test] - fn app_data_root_uses_ios_mobile_native_layout() { - let home = PathBuf::from("/var/mobile/Containers/Data/Application/example"); - - assert_eq!( - app_data_root_from_home(home.as_path()).expect("app data root"), - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app" - ) - ); - } - - #[test] - fn mobile_paths_follow_shared_logical_root_model() { - let home = PathBuf::from("/var/mobile/Containers/Data/Application/example"); - let paths = app_paths_from_home(home.as_path()).expect("mobile paths"); - - assert_eq!( - paths.config, - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/config/apps/app" - ) - ); - assert_eq!( - paths.data, - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/data/apps/app" - ) - ); - assert_eq!( - paths.secrets, - PathBuf::from( - "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/secrets/apps/app" - ) - ); - } -} diff --git a/crates/launchers/web/Cargo.toml b/crates/launchers/web/Cargo.toml @@ -1,32 +0,0 @@ -[package] -name = "radroots_app_web" -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -authors.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots web launcher" -publish = false - -[lib] -path = "src/lib.rs" -crate-type = ["cdylib", "rlib"] - -[dependencies] -eframe = { workspace = true, features = ["wgpu"] } -js-sys = "0.3.91" -log.workspace = true -radroots_app_core = { path = "../../shared/core" } -radroots_geocoder.workspace = true -wasm-bindgen-futures.workspace = true -web-sys = { workspace = true, features = ["Document", "Element", "HtmlCanvasElement", "Response", "Window"] } - -[target.'cfg(target_arch = "wasm32")'.dependencies] -nostr.workspace = true -nostr-browser-signer.workspace = true -wgpu = { workspace = true, features = ["std", "wgsl", "webgpu", "fragile-send-sync-non-atomic-wasm"] } - -[lints] -workspace = true diff --git a/crates/launchers/web/Trunk.toml b/crates/launchers/web/Trunk.toml @@ -1,3 +0,0 @@ -[build] -target = "index.html" -dist = "dist" diff --git a/crates/launchers/web/index.html b/crates/launchers/web/index.html @@ -1,65 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <meta - name="viewport" - content="width=device-width, initial-scale=1.0, viewport-fit=cover" - /> - <title>Rad Roots</title> - <base data-trunk-public-url /> - <style> - html, - body { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - background: #0e1116; - color: #f4efe1; - font-family: ui-sans-serif, system-ui, sans-serif; - } - - body { - display: grid; - place-items: center; - } - - #app-shell { - position: relative; - width: 100%; - height: 100%; - } - - #radroots_app_canvas { - width: 100%; - height: 100%; - display: block; - } - - #loading_text { - position: absolute; - inset: 0; - display: grid; - place-items: center; - letter-spacing: 0.08em; - text-transform: uppercase; - font-size: 0.75rem; - } - </style> - </head> - <body> - <div id="app-shell"> - <canvas id="radroots_app_canvas"></canvas> - <div id="loading_text">loading rad roots…</div> - </div> - <link data-trunk rel="copy-dir" href="../../assets" /> - <link - data-trunk - rel="rust" - href="Cargo.toml" - data-bin="radroots_app_web" - data-wasm-opt="0" - /> - </body> -</html> diff --git a/crates/launchers/web/src/lib.rs b/crates/launchers/web/src/lib.rs @@ -1,760 +0,0 @@ -#![forbid(unsafe_code)] - -#[cfg(target_arch = "wasm32")] -use std::cell::RefCell; -#[cfg(target_arch = "wasm32")] -use std::rc::Rc; - -#[cfg(target_arch = "wasm32")] -use eframe::wasm_bindgen::{JsCast as _, JsValue}; -#[cfg(target_arch = "wasm32")] -use js_sys::Uint8Array; -#[cfg(target_arch = "wasm32")] -use nostr::nips::nip19::ToBech32; -#[cfg(target_arch = "wasm32")] -use nostr::signer::NostrSigner; -#[cfg(target_arch = "wasm32")] -use nostr_browser_signer::{BrowserSigner, Error as BrowserSignerError}; -#[cfg(target_arch = "wasm32")] -use radroots_app_core::{ - HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, RadrootsAccountCustody, - RadrootsAccountSummary, RadrootsApp, RadrootsAppBackend, RadrootsLocationCountry, - RadrootsLocationCountryCenterLookupResult, RadrootsLocationCountryListResult, - RadrootsLocationPoint, RadrootsLocationResolverError, RadrootsLocationReverseOptions, - RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, SetupActionState, -}; -#[cfg(any(target_arch = "wasm32", test))] -use radroots_app_core::{ - RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState, - RadrootsOfflineGeocoderUnavailableKind, -}; -#[cfg(target_arch = "wasm32")] -use radroots_geocoder::{ - Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, - GeocoderReverseResult, -}; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_futures::JsFuture; - -#[cfg(target_arch = "wasm32")] -const GEOCODER_DB_ASSET_PATH: &str = "assets/geocoder/geonames.db"; -#[cfg(target_arch = "wasm32")] -const GEOCODER_REVISION_ASSET_PATH: &str = "assets/geocoder/geonames.revision"; - -#[cfg(any(target_arch = "wasm32", test))] -fn offline_geocoder_missing_build_asset_state( - debug_message: impl Into<String>, -) -> RadrootsOfflineGeocoderState { - RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Web, - debug_message, - ) -} - -#[cfg(any(target_arch = "wasm32", test))] -fn offline_geocoder_initialization_failed_state( - asset_revision: impl Into<String>, - debug_message: impl Into<String>, -) -> RadrootsOfflineGeocoderState { - RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Web, - asset_revision, - debug_message, - ) -} - -#[cfg(any(target_arch = "wasm32", test))] -fn is_valid_asset_revision(revision: &str) -> bool { - revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()) -} - -#[cfg(target_arch = "wasm32")] -fn js_error_message(value: JsValue) -> String { - value - .as_string() - .unwrap_or_else(|| "javascript error".to_owned()) -} - -#[cfg(target_arch = "wasm32")] -async fn fetch_response(path: &str) -> Result<web_sys::Response, String> { - let window = web_sys::window().ok_or_else(|| "window unavailable".to_owned())?; - let response_value = JsFuture::from(window.fetch_with_str(path)) - .await - .map_err(|err| format!("failed to fetch {path}: {}", js_error_message(err)))?; - let response = response_value - .dyn_into::<web_sys::Response>() - .map_err(|_| format!("fetch for {path} did not return a response"))?; - if !response.ok() { - return Err(format!( - "fetch for {path} failed with http {}", - response.status() - )); - } - Ok(response) -} - -#[cfg(target_arch = "wasm32")] -async fn fetch_text_asset(path: &str) -> Result<String, String> { - let response = fetch_response(path).await?; - let text_promise = response.text().map_err(|err| { - format!( - "failed to read text body for {path}: {}", - js_error_message(err) - ) - })?; - let text_value = JsFuture::from(text_promise).await.map_err(|err| { - format!( - "failed to load text asset {path}: {}", - js_error_message(err) - ) - })?; - text_value - .as_string() - .ok_or_else(|| format!("text asset {path} did not decode to a string")) -} - -#[cfg(target_arch = "wasm32")] -async fn fetch_bytes_asset(path: &str) -> Result<Vec<u8>, String> { - let response = fetch_response(path).await?; - let buffer_promise = response.array_buffer().map_err(|err| { - format!( - "failed to read binary body for {path}: {}", - js_error_message(err) - ) - })?; - let buffer = JsFuture::from(buffer_promise).await.map_err(|err| { - format!( - "failed to load binary asset {path}: {}", - js_error_message(err) - ) - })?; - Ok(Uint8Array::new(&buffer).to_vec()) -} - -#[cfg(target_arch = "wasm32")] -async fn initialize_offline_geocoder() -> Result<Geocoder, RadrootsOfflineGeocoderState> { - let revision_text = fetch_text_asset(GEOCODER_REVISION_ASSET_PATH) - .await - .map_err(offline_geocoder_missing_build_asset_state)?; - let revision = revision_text.trim().to_owned(); - if !is_valid_asset_revision(revision.as_str()) { - return Err(offline_geocoder_missing_build_asset_state(format!( - "web geocoder revision asset invalid at {GEOCODER_REVISION_ASSET_PATH}" - ))); - } - - let bytes = fetch_bytes_asset(GEOCODER_DB_ASSET_PATH) - .await - .map_err(|debug_message| { - RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, - RadrootsOfflineGeocoderPlatform::Web, - revision.clone(), - debug_message, - ) - })?; - - Geocoder::open_bytes(bytes.as_slice()).map_err(|source| { - offline_geocoder_initialization_failed_state( - revision, - format!("failed to open wasm geocoder from {GEOCODER_DB_ASSET_PATH}: {source}"), - ) - }) -} - -#[cfg(target_arch = "wasm32")] -fn map_reverse_result(result: GeocoderReverseResult) -> RadrootsResolvedLocation { - RadrootsResolvedLocation { - id: result.id, - name: result.name, - admin1_id: result.admin1_id, - admin1_name: result.admin1_name, - country_id: result.country_id, - country_name: result.country_name, - point: RadrootsLocationPoint { - lat: result.latitude, - lng: result.longitude, - }, - } -} - -#[cfg(target_arch = "wasm32")] -fn map_country_result(result: GeocoderCountryListResult) -> RadrootsLocationCountry { - RadrootsLocationCountry { - country_id: result.country_id, - country_name: result.country, - center: RadrootsLocationPoint { - lat: result.lat, - lng: result.lng, - }, - } -} - -#[cfg(target_arch = "wasm32")] -fn map_country_center_error(source: GeocoderError) -> RadrootsLocationResolverError { - match source { - GeocoderError::CountryCenterNotFound { country_id } => { - RadrootsLocationResolverError::CountryCenterNotFound { country_id } - } - other => RadrootsLocationResolverError::QueryFailed { - message: other.to_string(), - }, - } -} - -#[cfg(target_arch = "wasm32")] -#[derive(Clone)] -struct ConnectedSigner { - account_id: String, - npub: String, - signer: BrowserSigner, -} - -#[cfg(target_arch = "wasm32")] -enum WebConnectionState { - Disconnected, - Connecting, - Ready(ConnectedSigner), -} - -#[cfg(target_arch = "wasm32")] -struct WebBackendState { - connection: WebConnectionState, - pending_result: Option<Result<ConnectedSigner, String>>, - offline_geocoder_state: RadrootsOfflineGeocoderState, - pending_offline_geocoder_update: Option<RadrootsOfflineGeocoderState>, - geocoder: Option<Rc<Geocoder>>, - pending_reverse_lookup_result: Option<RadrootsReverseLocationLookupResult>, - pending_country_list_result: Option<RadrootsLocationCountryListResult>, - pending_country_center_result: Option<RadrootsLocationCountryCenterLookupResult>, -} - -#[cfg(target_arch = "wasm32")] -#[derive(Clone)] -struct WebBackend { - state: Rc<RefCell<WebBackendState>>, -} - -#[cfg(target_arch = "wasm32")] -impl WebBackend { - fn new() -> Self { - let backend = Self { - state: Rc::new(RefCell::new(WebBackendState { - connection: WebConnectionState::Disconnected, - pending_result: None, - offline_geocoder_state: RadrootsOfflineGeocoderState::Initializing, - pending_offline_geocoder_update: None, - geocoder: None, - pending_reverse_lookup_result: None, - pending_country_list_result: None, - pending_country_center_result: None, - })), - }; - backend.start_offline_geocoder_init(); - backend - } - - fn identity_state_for_ready(connected: &ConnectedSigner) -> IdentityGateState { - let _ = &connected.signer; - IdentityGateState::Ready { - account_id: connected.account_id.clone(), - } - } - - fn account_summary_for_ready(connected: &ConnectedSigner) -> RadrootsAccountSummary { - RadrootsAccountSummary { - account_id: connected.account_id.clone(), - npub: connected.npub.clone(), - label: Some("browser signer".to_owned()), - custody: RadrootsAccountCustody::BrowserSigner, - } - } - - fn connect_error_message(err: BrowserSignerError) -> String { - match err { - BrowserSignerError::NoGlobalWindowObject | BrowserSignerError::NamespaceNotFound(_) => { - "No NIP-07 browser signer detected.".to_owned() - } - other => format!("Browser signer connection failed: {other}"), - } - } - - fn disconnect_signer(&self) -> IdentityGateState { - let mut state = self.state.borrow_mut(); - state.connection = WebConnectionState::Disconnected; - state.pending_result = None; - IdentityGateState::Missing - } - - fn start_offline_geocoder_init(&self) { - let shared_state = Rc::clone(&self.state); - wasm_bindgen_futures::spawn_local(async move { - let result = initialize_offline_geocoder().await; - let mut state = shared_state.borrow_mut(); - match result { - Ok(geocoder) => { - state.geocoder = Some(Rc::new(geocoder)); - state.offline_geocoder_state = RadrootsOfflineGeocoderState::Ready; - state.pending_offline_geocoder_update = - Some(RadrootsOfflineGeocoderState::Ready); - } - Err(offline_geocoder_state) => { - state.geocoder = None; - state.offline_geocoder_state = offline_geocoder_state.clone(); - state.pending_offline_geocoder_update = Some(offline_geocoder_state); - } - } - }); - } - - fn ready_geocoder(&self) -> Result<Rc<Geocoder>, RadrootsLocationResolverError> { - let state = self.state.borrow(); - match &state.offline_geocoder_state { - RadrootsOfflineGeocoderState::Initializing => { - Err(RadrootsLocationResolverError::Initializing) - } - RadrootsOfflineGeocoderState::Unavailable { .. } => { - Err(RadrootsLocationResolverError::Unavailable) - } - RadrootsOfflineGeocoderState::Ready => { - state - .geocoder - .clone() - .ok_or_else(|| RadrootsLocationResolverError::QueryFailed { - message: "web geocoder was ready without an initialized engine".to_owned(), - }) - } - } - } -} - -#[cfg(target_arch = "wasm32")] -impl RadrootsAppBackend for WebBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Ready(connected) => Ok(Self::identity_state_for_ready(connected)), - WebConnectionState::Disconnected | WebConnectionState::Connecting => { - Ok(IdentityGateState::Missing) - } - } - } - - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Ready(connected) => { - Ok(vec![Self::account_summary_for_ready(connected)]) - } - WebConnectionState::Disconnected | WebConnectionState::Connecting => Ok(Vec::new()), - } - } - - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - Some(self.state.borrow().offline_geocoder_state.clone()) - } - - fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - Ok(self - .state - .borrow_mut() - .pending_offline_geocoder_update - .take()) - } - - fn reverse_location( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - let options = options.map(|options| GeocoderReverseOptions { - limit: options.limit, - degree_offset: options.degree_offset, - }); - geocoder - .reverse( - GeocoderPoint { - lat: point.lat, - lng: point.lng, - }, - options, - ) - .map(|results| results.into_iter().map(map_reverse_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) - } - - fn request_reverse_location_lookup( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - { - let mut state = self.state.borrow_mut(); - state.pending_reverse_lookup_result = None; - } - let shared_state = Rc::clone(&self.state); - wasm_bindgen_futures::spawn_local(async move { - let options = options.map(|options| GeocoderReverseOptions { - limit: options.limit, - degree_offset: options.degree_offset, - }); - let result = geocoder - .reverse( - GeocoderPoint { - lat: point.lat, - lng: point.lng, - }, - options, - ) - .map(|results| results.into_iter().map(map_reverse_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }); - shared_state.borrow_mut().pending_reverse_lookup_result = Some(result); - }); - Ok(()) - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(self.state.borrow_mut().pending_reverse_lookup_result.take()) - } - - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - { - let mut state = self.state.borrow_mut(); - state.pending_country_list_result = None; - } - let shared_state = Rc::clone(&self.state); - wasm_bindgen_futures::spawn_local(async move { - let result = geocoder - .country_list() - .map(|results| results.into_iter().map(map_country_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }); - shared_state.borrow_mut().pending_country_list_result = Some(result); - }); - Ok(()) - } - - fn poll_location_country_list_result( - &self, - ) -> Result<Option<RadrootsLocationCountryListResult>, String> { - Ok(self.state.borrow_mut().pending_country_list_result.take()) - } - - fn request_location_country_center_lookup( - &self, - country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - { - let mut state = self.state.borrow_mut(); - state.pending_country_center_result = None; - } - let shared_state = Rc::clone(&self.state); - let country_id = country_id.to_owned(); - wasm_bindgen_futures::spawn_local(async move { - let result = geocoder - .country_center(country_id.as_str()) - .map(|point| RadrootsLocationPoint { - lat: point.lat, - lng: point.lng, - }) - .map_err(map_country_center_error); - shared_state.borrow_mut().pending_country_center_result = Some(result); - }); - Ok(()) - } - - fn poll_location_country_center_lookup_result( - &self, - ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { - Ok(self.state.borrow_mut().pending_country_center_result.take()) - } - - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - geocoder - .country_list() - .map(|results| results.into_iter().map(map_country_result).collect()) - .map_err(|source| RadrootsLocationResolverError::QueryFailed { - message: source.to_string(), - }) - } - - fn location_country_center( - &self, - country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - let geocoder = self.ready_geocoder()?; - geocoder - .country_center(country_id) - .map(|point| RadrootsLocationPoint { - lat: point.lat, - lng: point.lng, - }) - .map_err(map_country_center_error) - } - - fn setup_action_state(&self) -> SetupActionState { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Connecting => SetupActionState { - label: "Connecting Browser Signer...".to_owned(), - enabled: false, - pending: true, - }, - WebConnectionState::Disconnected => SetupActionState { - label: "Connect Browser Signer".to_owned(), - enabled: true, - pending: false, - }, - WebConnectionState::Ready(_) => SetupActionState { - label: "Browser Signer Connected".to_owned(), - enabled: false, - pending: false, - }, - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Connecting => return Ok(None), - WebConnectionState::Ready(connected) => { - return Ok(Some(Self::identity_state_for_ready(connected))); - } - WebConnectionState::Disconnected => {} - } - } - - let signer = BrowserSigner::new().map_err(Self::connect_error_message)?; - { - let mut state = self.state.borrow_mut(); - state.connection = WebConnectionState::Connecting; - state.pending_result = None; - } - - let shared_state = Rc::clone(&self.state); - wasm_bindgen_futures::spawn_local(async move { - let result = match signer.get_public_key().await { - Ok(public_key) => match public_key.to_bech32() { - Ok(npub) => Ok(ConnectedSigner { - account_id: public_key.to_hex(), - npub, - signer, - }), - Err(source) => Err(format!("Failed to encode npub: {source}")), - }, - Err(source) => Err(format!("Browser signer connection failed: {source}")), - }; - - let mut state = shared_state.borrow_mut(); - state.pending_result = Some(result); - }); - - Ok(None) - } - - fn home_action_states(&self) -> Vec<HomeActionState> { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Ready(_) => vec![HomeActionState { - kind: HomeActionKind::DisconnectSigner, - label: "Disconnect Browser Signer".to_owned(), - enabled: true, - pending: false, - }], - WebConnectionState::Disconnected | WebConnectionState::Connecting => Vec::new(), - } - } - - fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> { - match action { - HomeActionKind::DisconnectSigner => { - Ok(HomeActionResult::IdentityState(self.disconnect_signer())) - } - HomeActionKind::BackupSecretKey - | HomeActionKind::RevealRawSecretKey - | HomeActionKind::RemoveLocalKey - | HomeActionKind::ResetDevice => Ok(HomeActionResult::None), - } - } - - fn request_select_account( - &self, - account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - let state = self.state.borrow(); - match &state.connection { - WebConnectionState::Ready(connected) if connected.account_id == account_id => { - Ok(Some(Self::identity_state_for_ready(connected))) - } - WebConnectionState::Ready(_) => Err("unknown browser signer account".to_owned()), - WebConnectionState::Disconnected | WebConnectionState::Connecting => Ok(None), - } - } - - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - let mut state = self.state.borrow_mut(); - let Some(result) = state.pending_result.take() else { - return Ok(None); - }; - - match result { - Ok(connected) => { - let identity = Self::identity_state_for_ready(&connected); - state.connection = WebConnectionState::Ready(connected); - Ok(Some(identity)) - } - Err(err) => { - state.connection = WebConnectionState::Disconnected; - Err(err) - } - } - } -} - -#[cfg(target_arch = "wasm32")] -fn loading_text_element() -> Option<web_sys::Element> { - let window = web_sys::window()?; - let document = window.document()?; - document.get_element_by_id("loading_text") -} - -#[cfg(target_arch = "wasm32")] -fn clear_loading_text() { - if let Some(loading_text) = loading_text_element() { - loading_text.remove(); - } -} - -#[cfg(target_arch = "wasm32")] -fn show_loading_failure() { - if let Some(loading_text) = loading_text_element() { - loading_text.set_inner_html("<p>failed to start radroots app</p>"); - } -} - -#[cfg(target_arch = "wasm32")] -async fn launch_app() -> Result<(), String> { - let web_options = eframe::WebOptions::default(); - let window = web_sys::window().ok_or_else(|| "window unavailable".to_owned())?; - let document = window - .document() - .ok_or_else(|| "document unavailable".to_owned())?; - let canvas = document - .get_element_by_id("radroots_app_canvas") - .ok_or_else(|| "radroots_app_canvas missing".to_owned())? - .dyn_into::<web_sys::HtmlCanvasElement>() - .map_err(|_| "radroots_app_canvas is not a canvas element".to_owned())?; - - eframe::WebRunner::new() - .start( - canvas, - web_options, - Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(WebBackend::new()))))), - ) - .await - .map_err(|err| format!("failed to start radroots app: {err:?}")) -} - -#[cfg(target_arch = "wasm32")] -pub fn launch() { - let log_level = if cfg!(debug_assertions) { - log::LevelFilter::Info - } else { - log::LevelFilter::Warn - }; - let _ = eframe::WebLogger::init(log_level); - - wasm_bindgen_futures::spawn_local(async { - match launch_app().await { - Ok(()) => clear_loading_text(), - Err(err) => { - log::error!("{err}"); - show_loading_failure(); - } - } - }); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn missing_build_asset_state_is_stable() { - let state = - offline_geocoder_missing_build_asset_state("web geocoder asset missing from build"); - - assert_eq!(state.summary_label(), "Offline geocoder unavailable"); - assert_eq!( - state.user_message(), - Some("Offline geocoder is not available in this build.") - ); - assert_eq!( - state.technical_message(), - Some("The offline geocoder data file is missing from this app build.") - ); - assert_eq!( - state.debug_message(), - Some("web geocoder asset missing from build") - ); - } - - #[test] - fn wasm_revision_validation_matches_stamped_sha256_contract() { - assert!(is_valid_asset_revision( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" - )); - assert!(!is_valid_asset_revision("abcd")); - assert!(!is_valid_asset_revision( - "not-a-valid-revision-because-it-is-not-hexadecimal-or-64-bytes-long" - )); - } - - #[test] - fn initialization_failed_state_includes_revision_context() { - let state = offline_geocoder_initialization_failed_state( - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c", - "failed to open wasm geocoder bytes", - ); - let diagnostic = state.diagnostic().expect("diagnostic"); - - assert_eq!(diagnostic.platform_code, "web"); - assert_eq!( - diagnostic.asset_revision.as_deref(), - Some("6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c") - ); - assert_eq!(diagnostic.code, "initialization_failed"); - assert_eq!( - state.debug_message(), - Some("failed to open wasm geocoder bytes") - ); - } - - #[test] - fn location_resolver_unavailable_code_is_stable() { - assert_eq!( - radroots_app_core::RadrootsLocationResolverError::Unavailable.code(), - "unavailable" - ); - } -} - -#[cfg(not(target_arch = "wasm32"))] -pub fn launch() {} diff --git a/crates/launchers/web/src/main.rs b/crates/launchers/web/src/main.rs @@ -1,5 +0,0 @@ -#![forbid(unsafe_code)] - -fn main() { - radroots_app_web::launch(); -} diff --git a/crates/shared/core/Cargo.toml b/crates/shared/core/Cargo.toml @@ -1,23 +0,0 @@ -[package] -name = "radroots_app_core" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots application core" -publish = false - -[lints] -workspace = true - -[dependencies] -eframe.workspace = true -egui.workspace = true -radroots_runtime_paths.workspace = true -zeroize.workspace = true - -[dev-dependencies] -radroots_app_test_support = { path = "../test_support" } diff --git a/crates/shared/core/src/account_roster.rs b/crates/shared/core/src/account_roster.rs @@ -1,37 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RadrootsAccountCustody { - LocalManaged, - BrowserSigner, - RemoteSigner, -} - -impl RadrootsAccountCustody { - pub fn label(self) -> &'static str { - match self { - Self::LocalManaged => "local managed", - Self::BrowserSigner => "browser signer", - Self::RemoteSigner => "remote signer", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsAccountSummary { - pub account_id: String, - pub npub: String, - pub label: Option<String>, - pub custody: RadrootsAccountCustody, -} - -impl RadrootsAccountSummary { - pub fn display_label(&self) -> String { - match self.label.as_deref() { - Some(label) if !label.trim().is_empty() => label.to_owned(), - _ => match self.custody { - RadrootsAccountCustody::LocalManaged => "local account".to_owned(), - RadrootsAccountCustody::BrowserSigner => "browser signer".to_owned(), - RadrootsAccountCustody::RemoteSigner => "remote signer".to_owned(), - }, - } - } -} diff --git a/crates/shared/core/src/home_location_tools/country_lookup.rs b/crates/shared/core/src/home_location_tools/country_lookup.rs @@ -1,536 +0,0 @@ -use crate::{ - RadrootsAppBackend, RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult, - RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsOfflineGeocoderState, -}; -use eframe::egui; - -#[derive(Debug, Clone, PartialEq)] -enum CountryListState { - Idle, - Pending, - Ready(Vec<RadrootsLocationCountry>), - Failed { message: String }, -} - -impl Default for CountryListState { - fn default() -> Self { - Self::Idle - } -} - -#[derive(Debug, Clone, PartialEq)] -struct CountryCenterLookupResult { - country_id: String, - country_name: Option<String>, - center: RadrootsLocationPoint, -} - -#[derive(Debug, Clone, PartialEq)] -enum CountryCenterState { - Idle, - Pending { country_id: String }, - Ready(CountryCenterLookupResult), - Failed { message: String }, -} - -impl Default for CountryCenterState { - fn default() -> Self { - Self::Idle - } -} - -#[derive(Debug, Default, Clone, PartialEq)] -pub(super) struct CountryLookupTools { - countries: CountryListState, - selected_country_id: Option<String>, - center: CountryCenterState, -} - -impl CountryLookupTools { - pub(super) fn clear(&mut self) { - self.countries = CountryListState::Idle; - self.selected_country_id = None; - self.center = CountryCenterState::Idle; - } - - pub(super) fn render( - &mut self, - ui: &mut egui::Ui, - backend: &dyn RadrootsAppBackend, - offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>, - ) { - ui.add_space(20.0); - ui.label("Offline country lookup"); - ui.add_space(8.0); - ui.label("Load country data and resolve a country center using the on-device geocoder."); - ui.add_space(8.0); - - let load_enabled = - is_country_action_enabled(offline_geocoder_state) && !self.is_list_pending(); - if ui - .add_enabled(load_enabled, egui::Button::new(self.load_button_label())) - .clicked() - { - self.begin_load_countries(backend); - } - - if let Some(helper_message) = availability_message(offline_geocoder_state) { - ui.add_space(8.0); - ui.label(helper_message); - } - - if let Some(message) = self.list_status_message() { - ui.add_space(8.0); - ui.label(message); - } - - if let Some(countries) = self.ready_countries().cloned() { - ui.add_space(8.0); - let selected_country_id = &mut self.selected_country_id; - let selected_text = - country_label_for_id(countries.as_slice(), selected_country_id.as_deref()); - egui::ComboBox::from_label("Country") - .selected_text(selected_text) - .show_ui(ui, |ui| { - for country in countries.as_slice() { - let response = ui.selectable_value( - selected_country_id, - Some(country.country_id.clone()), - country_label(country), - ); - if response.clicked() { - self.center = CountryCenterState::Idle; - } - } - }); - - ui.add_space(8.0); - let center_enabled = - is_country_action_enabled(offline_geocoder_state) && !self.is_center_pending(); - if ui - .add_enabled( - center_enabled, - egui::Button::new(self.center_button_label()), - ) - .clicked() - { - self.begin_resolve_country_center(backend); - } - } - - if let Some(message) = self.center_status_message() { - ui.add_space(8.0); - ui.label(message); - } - - if let Some(result) = self.center_result() { - ui.add_space(12.0); - ui.label( - result - .country_name - .as_deref() - .unwrap_or(result.country_id.as_str()), - ); - ui.monospace(format!( - "{}, {}", - format_coordinate(result.center.lat), - format_coordinate(result.center.lng), - )); - } - } - - pub(super) fn apply_list_result(&mut self, result: RadrootsLocationCountryListResult) { - match result { - Ok(countries) if countries.is_empty() => { - self.countries = CountryListState::Failed { - message: "No offline countries are available.".to_owned(), - }; - self.selected_country_id = None; - self.center = CountryCenterState::Idle; - } - Ok(countries) => { - self.selected_country_id = selected_country_id_after_refresh( - self.selected_country_id.as_deref(), - countries.as_slice(), - ); - self.countries = CountryListState::Ready(countries); - self.center = CountryCenterState::Idle; - } - Err(error) => { - self.countries = CountryListState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - pub(super) fn apply_list_poll_error(&mut self, message: String) { - self.countries = CountryListState::Failed { message }; - } - - pub(super) fn apply_center_result( - &mut self, - result: RadrootsLocationCountryCenterLookupResult, - ) { - let country_id = match &self.center { - CountryCenterState::Pending { country_id } => country_id.clone(), - CountryCenterState::Idle - | CountryCenterState::Ready(_) - | CountryCenterState::Failed { .. } => return, - }; - - match result { - Ok(center) => { - self.center = CountryCenterState::Ready(CountryCenterLookupResult { - country_name: self.country_name_for_id(country_id.as_str()), - country_id, - center, - }); - } - Err(error) => { - self.center = CountryCenterState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - pub(super) fn apply_center_poll_error(&mut self, message: String) { - self.center = CountryCenterState::Failed { message }; - } - - pub(super) fn is_pending(&self) -> bool { - self.is_list_pending() || self.is_center_pending() - } - - fn begin_load_countries(&mut self, backend: &dyn RadrootsAppBackend) { - self.countries = CountryListState::Idle; - self.center = CountryCenterState::Idle; - - match backend.request_location_country_list() { - Ok(()) => { - self.countries = CountryListState::Pending; - } - Err(error) => { - self.countries = CountryListState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - fn begin_resolve_country_center(&mut self, backend: &dyn RadrootsAppBackend) { - let Some(country_id) = self.selected_country_id.clone() else { - self.center = CountryCenterState::Failed { - message: "Select a country first.".to_owned(), - }; - return; - }; - - match backend.request_location_country_center_lookup(country_id.as_str()) { - Ok(()) => { - self.center = CountryCenterState::Pending { country_id }; - } - Err(error) => { - self.center = CountryCenterState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - fn is_list_pending(&self) -> bool { - matches!(self.countries, CountryListState::Pending) - } - - fn is_center_pending(&self) -> bool { - matches!(self.center, CountryCenterState::Pending { .. }) - } - - fn load_button_label(&self) -> &'static str { - if self.is_list_pending() { - "Loading Offline Countries..." - } else { - "Load Offline Countries" - } - } - - fn center_button_label(&self) -> &'static str { - if self.is_center_pending() { - "Resolving Country Center..." - } else { - "Resolve Country Center" - } - } - - fn list_status_message(&self) -> Option<&str> { - match &self.countries { - CountryListState::Idle | CountryListState::Ready(_) => None, - CountryListState::Pending => Some("Loading offline countries..."), - CountryListState::Failed { message } => Some(message.as_str()), - } - } - - fn center_status_message(&self) -> Option<&str> { - match &self.center { - CountryCenterState::Idle | CountryCenterState::Ready(_) => None, - CountryCenterState::Pending { .. } => Some("Resolving country center..."), - CountryCenterState::Failed { message } => Some(message.as_str()), - } - } - - fn ready_countries(&self) -> Option<&Vec<RadrootsLocationCountry>> { - match &self.countries { - CountryListState::Ready(countries) => Some(countries), - CountryListState::Idle - | CountryListState::Pending - | CountryListState::Failed { .. } => None, - } - } - - fn center_result(&self) -> Option<&CountryCenterLookupResult> { - match &self.center { - CountryCenterState::Ready(result) => Some(result), - CountryCenterState::Idle - | CountryCenterState::Pending { .. } - | CountryCenterState::Failed { .. } => None, - } - } - - fn country_name_for_id(&self, country_id: &str) -> Option<String> { - self.ready_countries() - .and_then(|countries| { - countries - .iter() - .find(|country| country.country_id == country_id) - .map(|country| country.country_name.clone()) - }) - .flatten() - } -} - -fn is_country_action_enabled(state: Option<&RadrootsOfflineGeocoderState>) -> bool { - matches!(state, Some(RadrootsOfflineGeocoderState::Ready)) -} - -fn availability_message(state: Option<&RadrootsOfflineGeocoderState>) -> Option<&str> { - match state { - Some(RadrootsOfflineGeocoderState::Initializing) => { - Some("Offline country lookup is still initializing on this device.") - } - Some(RadrootsOfflineGeocoderState::Unavailable { .. }) => { - state.and_then(RadrootsOfflineGeocoderState::user_message) - } - Some(RadrootsOfflineGeocoderState::Ready) | None => None, - } -} - -fn selected_country_id_after_refresh( - selected_country_id: Option<&str>, - countries: &[RadrootsLocationCountry], -) -> Option<String> { - if let Some(selected_country_id) = selected_country_id { - if countries - .iter() - .any(|country| country.country_id == selected_country_id) - { - return Some(selected_country_id.to_owned()); - } - } - - countries.first().map(|country| country.country_id.clone()) -} - -fn country_label(country: &RadrootsLocationCountry) -> String { - country - .country_name - .clone() - .unwrap_or_else(|| country.country_id.clone()) -} - -fn country_label_for_id(countries: &[RadrootsLocationCountry], country_id: Option<&str>) -> String { - country_id - .and_then(|country_id| { - countries - .iter() - .find(|country| country.country_id == country_id) - .map(country_label) - }) - .unwrap_or_else(|| "Select a country".to_owned()) -} - -fn format_coordinate(value: f64) -> String { - format!("{value:.4}") -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - IdentityGateState, RadrootsLocationResolverError, RadrootsReverseLocationLookupResult, - SetupActionState, - }; - use std::cell::RefCell; - use std::collections::VecDeque; - use std::rc::Rc; - - #[derive(Clone)] - struct CountryBackend { - list_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>, - center_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>, - requested_country_ids: Rc<RefCell<Vec<String>>>, - } - - impl RadrootsAppBackend for CountryBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - Ok(IdentityGateState::Missing) - } - - fn setup_action_state(&self) -> SetupActionState { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - - fn request_reverse_location_lookup( - &self, - _point: RadrootsLocationPoint, - _options: Option<crate::RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(None) - } - - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - self.list_request.borrow_mut().pop_front().unwrap_or(Ok(())) - } - - fn request_location_country_center_lookup( - &self, - country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - self.requested_country_ids - .borrow_mut() - .push(country_id.to_owned()); - self.center_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(())) - } - } - - fn country_backend( - list_request: Vec<Result<(), RadrootsLocationResolverError>>, - center_request: Vec<Result<(), RadrootsLocationResolverError>>, - ) -> (CountryBackend, Rc<RefCell<Vec<String>>>) { - let requested_country_ids = Rc::new(RefCell::new(Vec::new())); - ( - CountryBackend { - list_request: Rc::new(RefCell::new(list_request.into())), - center_request: Rc::new(RefCell::new(center_request.into())), - requested_country_ids: requested_country_ids.clone(), - }, - requested_country_ids, - ) - } - - #[test] - fn begin_load_countries_enters_pending_state() { - let (backend, _) = country_backend(vec![Ok(())], Vec::new()); - let mut tools = CountryLookupTools::default(); - - tools.begin_load_countries(&backend); - - assert_eq!( - tools.list_status_message(), - Some("Loading offline countries...") - ); - assert!(tools.is_pending()); - } - - #[test] - fn apply_list_result_selects_first_country() { - let mut tools = CountryLookupTools::default(); - - tools.apply_list_result(Ok(vec![ - sample_country("BR", Some("Brazil"), -14.235, -51.9253), - sample_country("KE", Some("Kenya"), 0.0236, 37.9062), - ])); - - assert_eq!(tools.selected_country_id.as_deref(), Some("BR")); - assert!(matches!(tools.ready_countries(), Some(countries) if countries.len() == 2)); - } - - #[test] - fn begin_resolve_country_center_uses_selected_country_id() { - let (backend, requested_country_ids) = country_backend(Vec::new(), vec![Ok(())]); - let mut tools = CountryLookupTools::default(); - tools.apply_list_result(Ok(vec![ - sample_country("BR", Some("Brazil"), -14.235, -51.9253), - sample_country("KE", Some("Kenya"), 0.0236, 37.9062), - ])); - tools.selected_country_id = Some("KE".to_owned()); - - tools.begin_resolve_country_center(&backend); - - assert_eq!(requested_country_ids.borrow().as_slice(), ["KE"]); - assert_eq!( - tools.center_status_message(), - Some("Resolving country center...") - ); - } - - #[test] - fn apply_center_result_records_country_center() { - let mut tools = CountryLookupTools::default(); - tools.apply_list_result(Ok(vec![sample_country( - "BR", - Some("Brazil"), - -14.235, - -51.9253, - )])); - tools.center = CountryCenterState::Pending { - country_id: "BR".to_owned(), - }; - - tools.apply_center_result(Ok(RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - })); - - let result = tools.center_result().expect("country center result"); - assert_eq!(result.country_id, "BR"); - assert_eq!(result.country_name.as_deref(), Some("Brazil")); - assert_eq!( - result.center, - RadrootsLocationPoint { - lat: -14.235, - lng: -51.9253, - } - ); - } - - fn sample_country( - country_id: &str, - country_name: Option<&str>, - lat: f64, - lng: f64, - ) -> RadrootsLocationCountry { - RadrootsLocationCountry { - country_id: country_id.to_owned(), - country_name: country_name.map(str::to_owned), - center: RadrootsLocationPoint { lat, lng }, - } - } -} diff --git a/crates/shared/core/src/home_location_tools/mod.rs b/crates/shared/core/src/home_location_tools/mod.rs @@ -1,101 +0,0 @@ -use crate::{ - RadrootsAppBackend, RadrootsLocationCountryCenterLookupResult, - RadrootsLocationCountryListResult, RadrootsOfflineGeocoderState, - RadrootsReverseLocationLookupResult, -}; -use eframe::egui; - -mod country_lookup; -mod reverse_lookup; - -use country_lookup::CountryLookupTools; -#[cfg(test)] -use reverse_lookup::HomeLocationLookupResult; -use reverse_lookup::ReverseLookupTools; - -#[derive(Debug, Default, Clone, PartialEq)] -pub(crate) struct HomeLocationTools { - country_lookup: CountryLookupTools, - reverse_lookup: ReverseLookupTools, -} - -impl HomeLocationTools { - pub(crate) fn new() -> Self { - Self::default() - } - - pub(crate) fn clear(&mut self) { - self.country_lookup.clear(); - self.reverse_lookup.clear(); - } - - #[cfg(test)] - pub(crate) fn set_query_inputs( - &mut self, - latitude: impl Into<String>, - longitude: impl Into<String>, - ) { - self.reverse_lookup.set_query_inputs(latitude, longitude); - } - - pub(crate) fn render( - &mut self, - ui: &mut egui::Ui, - backend: &dyn RadrootsAppBackend, - offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>, - ) { - self.reverse_lookup - .render(ui, backend, offline_geocoder_state); - self.country_lookup - .render(ui, backend, offline_geocoder_state); - } - - pub(crate) fn apply_reverse_lookup_result( - &mut self, - result: RadrootsReverseLocationLookupResult, - ) { - self.reverse_lookup.apply_result(result); - } - - pub(crate) fn apply_reverse_lookup_poll_error(&mut self, message: String) { - self.reverse_lookup.apply_poll_error(message); - } - - pub(crate) fn apply_country_list_result(&mut self, result: RadrootsLocationCountryListResult) { - self.country_lookup.apply_list_result(result); - } - - pub(crate) fn apply_country_list_poll_error(&mut self, message: String) { - self.country_lookup.apply_list_poll_error(message); - } - - pub(crate) fn apply_country_center_result( - &mut self, - result: RadrootsLocationCountryCenterLookupResult, - ) { - self.country_lookup.apply_center_result(result); - } - - pub(crate) fn apply_country_center_poll_error(&mut self, message: String) { - self.country_lookup.apply_center_poll_error(message); - } - - #[cfg(test)] - pub(crate) fn begin_resolve_with_backend(&mut self, backend: &dyn RadrootsAppBackend) { - self.reverse_lookup.begin_resolve_with_backend(backend); - } - - pub(crate) fn is_pending(&self) -> bool { - self.reverse_lookup.is_pending() || self.country_lookup.is_pending() - } - - #[cfg(test)] - pub(crate) fn status_message(&self) -> Option<&str> { - self.reverse_lookup.status_message() - } - - #[cfg(test)] - pub(crate) fn lookup_result(&self) -> Option<&HomeLocationLookupResult> { - self.reverse_lookup.lookup_result() - } -} diff --git a/crates/shared/core/src/home_location_tools/reverse_lookup.rs b/crates/shared/core/src/home_location_tools/reverse_lookup.rs @@ -1,440 +0,0 @@ -use crate::{ - RadrootsAppBackend, RadrootsLocationPoint, RadrootsLocationReverseOptions, - RadrootsOfflineGeocoderState, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, -}; -use eframe::egui; - -const HOME_LOOKUP_RESULT_LIMIT: usize = 3; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HomeLocationLookupResult { - pub queried_point: RadrootsLocationPoint, - pub matches: Vec<RadrootsResolvedLocation>, -} - -#[derive(Debug, Clone, PartialEq)] -enum HomeLocationLookupState { - Idle, - Pending { - queried_point: RadrootsLocationPoint, - }, - Ready(HomeLocationLookupResult), - Failed { - message: String, - }, -} - -impl Default for HomeLocationLookupState { - fn default() -> Self { - Self::Idle - } -} - -#[derive(Debug, Default, Clone, PartialEq)] -pub(super) struct ReverseLookupTools { - latitude_input: String, - longitude_input: String, - lookup_state: HomeLocationLookupState, -} - -impl ReverseLookupTools { - pub(super) fn clear(&mut self) { - self.latitude_input.clear(); - self.longitude_input.clear(); - self.lookup_state = HomeLocationLookupState::Idle; - } - - #[cfg(test)] - pub(super) fn set_query_inputs( - &mut self, - latitude: impl Into<String>, - longitude: impl Into<String>, - ) { - self.latitude_input = latitude.into(); - self.longitude_input = longitude.into(); - } - - pub(super) fn render( - &mut self, - ui: &mut egui::Ui, - backend: &dyn RadrootsAppBackend, - offline_geocoder_state: Option<&RadrootsOfflineGeocoderState>, - ) { - ui.add_space(20.0); - ui.label("Offline location lookup"); - ui.add_space(8.0); - ui.label("Resolve a latitude and longitude pair using the on-device geocoder."); - ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.label("Latitude"); - ui.add( - egui::TextEdit::singleline(&mut self.latitude_input) - .hint_text("12.34") - .desired_width(140.0), - ); - ui.add_space(8.0); - ui.label("Longitude"); - ui.add( - egui::TextEdit::singleline(&mut self.longitude_input) - .hint_text("-56.78") - .desired_width(140.0), - ); - }); - ui.add_space(8.0); - - let resolve_enabled = is_resolve_enabled(offline_geocoder_state) && !self.is_pending(); - if ui - .add_enabled( - resolve_enabled, - egui::Button::new(self.resolve_button_label()), - ) - .clicked() - { - self.begin_resolve_with_backend(backend); - } - - if let Some(helper_message) = availability_message(offline_geocoder_state) { - ui.add_space(8.0); - ui.label(helper_message); - } - - if let Some(message) = self.status_message() { - ui.add_space(8.0); - ui.label(message); - } - - if let Some(result) = self.lookup_result() { - ui.add_space(12.0); - ui.label(format!( - "Query: {}, {}", - format_coordinate(result.queried_point.lat), - format_coordinate(result.queried_point.lng), - )); - for resolved in result.matches.iter().take(HOME_LOOKUP_RESULT_LIMIT) { - ui.add_space(8.0); - ui.label(resolved.name.as_str()); - if let Some(admin1_name) = &resolved.admin1_name { - ui.label(admin1_name.as_str()); - } - if let Some(country_name) = &resolved.country_name { - ui.label(country_name.as_str()); - } else { - ui.label(resolved.country_id.as_str()); - } - ui.monospace(format!( - "{}, {}", - format_coordinate(resolved.point.lat), - format_coordinate(resolved.point.lng), - )); - } - } - } - - pub(super) fn begin_resolve_with_backend(&mut self, backend: &dyn RadrootsAppBackend) { - self.lookup_state = HomeLocationLookupState::Idle; - - let query_point = match self.parse_query_point() { - Ok(point) => point, - Err(message) => { - self.lookup_state = HomeLocationLookupState::Failed { message }; - return; - } - }; - - let options = RadrootsLocationReverseOptions { - limit: HOME_LOOKUP_RESULT_LIMIT, - ..RadrootsLocationReverseOptions::default() - }; - match backend.request_reverse_location_lookup(query_point, Some(options)) { - Ok(()) => { - self.lookup_state = HomeLocationLookupState::Pending { - queried_point: query_point, - }; - } - Err(error) => { - self.lookup_state = HomeLocationLookupState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - pub(super) fn apply_result(&mut self, result: RadrootsReverseLocationLookupResult) { - let queried_point = match self.lookup_state { - HomeLocationLookupState::Pending { queried_point } => queried_point, - HomeLocationLookupState::Idle - | HomeLocationLookupState::Ready(_) - | HomeLocationLookupState::Failed { .. } => return, - }; - - match result { - Ok(matches) if matches.is_empty() => { - self.lookup_state = HomeLocationLookupState::Failed { - message: "No offline location matched that coordinate.".to_owned(), - }; - } - Ok(matches) => { - self.lookup_state = HomeLocationLookupState::Ready(HomeLocationLookupResult { - queried_point, - matches, - }); - } - Err(error) => { - self.lookup_state = HomeLocationLookupState::Failed { - message: error.user_message().to_owned(), - }; - } - } - } - - pub(super) fn apply_poll_error(&mut self, message: String) { - self.lookup_state = HomeLocationLookupState::Failed { message }; - } - - pub(super) fn is_pending(&self) -> bool { - matches!(self.lookup_state, HomeLocationLookupState::Pending { .. }) - } - - fn parse_query_point(&self) -> Result<RadrootsLocationPoint, String> { - let lat = parse_coordinate(self.latitude_input.as_str(), "latitude", -90.0, 90.0)?; - let lng = parse_coordinate(self.longitude_input.as_str(), "longitude", -180.0, 180.0)?; - Ok(RadrootsLocationPoint { lat, lng }) - } - - fn resolve_button_label(&self) -> &'static str { - if self.is_pending() { - "Resolving Offline Location..." - } else { - "Resolve Offline Location" - } - } - - pub(super) fn status_message(&self) -> Option<&str> { - match &self.lookup_state { - HomeLocationLookupState::Idle | HomeLocationLookupState::Ready(_) => None, - HomeLocationLookupState::Pending { .. } => Some("Resolving offline location..."), - HomeLocationLookupState::Failed { message } => Some(message.as_str()), - } - } - - pub(super) fn lookup_result(&self) -> Option<&HomeLocationLookupResult> { - match &self.lookup_state { - HomeLocationLookupState::Ready(result) => Some(result), - HomeLocationLookupState::Idle - | HomeLocationLookupState::Pending { .. } - | HomeLocationLookupState::Failed { .. } => None, - } - } -} - -fn is_resolve_enabled(state: Option<&RadrootsOfflineGeocoderState>) -> bool { - matches!(state, Some(RadrootsOfflineGeocoderState::Ready)) -} - -fn availability_message(state: Option<&RadrootsOfflineGeocoderState>) -> Option<&str> { - match state { - Some(RadrootsOfflineGeocoderState::Initializing) => { - Some("Offline location resolution is still initializing on this device.") - } - Some(RadrootsOfflineGeocoderState::Unavailable { .. }) => { - state.and_then(RadrootsOfflineGeocoderState::user_message) - } - Some(RadrootsOfflineGeocoderState::Ready) | None => None, - } -} - -fn parse_coordinate(raw: &str, label: &str, min: f64, max: f64) -> Result<f64, String> { - let trimmed = raw.trim(); - if trimmed.is_empty() { - return Err(format!("{label} is required")); - } - - let value = trimmed - .parse::<f64>() - .map_err(|_| format!("{label} must be a valid number"))?; - if !value.is_finite() { - return Err(format!("{label} must be a finite number")); - } - if value < min || value > max { - return Err(format!("{label} must be between {min} and {max}")); - } - - Ok(value) -} - -fn format_coordinate(value: f64) -> String { - format!("{value:.4}") -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - IdentityGateState, RadrootsLocationCountry, RadrootsLocationResolverError, SetupActionState, - }; - use std::cell::RefCell; - use std::rc::Rc; - - #[derive(Clone)] - struct ResolveBackend { - start_response: Result<(), RadrootsLocationResolverError>, - requested: Rc< - RefCell< - Vec<( - RadrootsLocationPoint, - Option<RadrootsLocationReverseOptions>, - )>, - >, - >, - } - - impl RadrootsAppBackend for ResolveBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - Ok(IdentityGateState::Missing) - } - - fn setup_action_state(&self) -> SetupActionState { - SetupActionState { - label: "Generate New Key".to_owned(), - enabled: true, - pending: false, - } - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - - fn request_reverse_location_lookup( - &self, - point: RadrootsLocationPoint, - options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - self.requested.borrow_mut().push((point, options)); - self.start_response.clone() - } - - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - - fn location_country_center( - &self, - _country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - } - - fn resolve_backend( - start_response: Result<(), RadrootsLocationResolverError>, - ) -> ( - ResolveBackend, - Rc< - RefCell< - Vec<( - RadrootsLocationPoint, - Option<RadrootsLocationReverseOptions>, - )>, - >, - >, - ) { - let requested = Rc::new(RefCell::new(Vec::new())); - ( - ResolveBackend { - start_response, - requested: requested.clone(), - }, - requested, - ) - } - - #[test] - fn begin_resolve_requests_three_results() { - let (backend, requested) = resolve_backend(Ok(())); - let mut tools = ReverseLookupTools::default(); - tools.set_query_inputs("12.5", "-42.25"); - - tools.begin_resolve_with_backend(&backend); - - let requested = requested.borrow(); - assert_eq!(requested.len(), 1); - assert_eq!( - requested[0].0, - RadrootsLocationPoint { - lat: 12.5, - lng: -42.25, - } - ); - assert_eq!( - requested[0].1, - Some(RadrootsLocationReverseOptions { - limit: 3, - ..RadrootsLocationReverseOptions::default() - }) - ); - assert!(tools.is_pending()); - } - - #[test] - fn begin_resolve_rejects_out_of_range_coordinates() { - let (backend, requested) = resolve_backend(Ok(())); - let mut tools = ReverseLookupTools::default(); - tools.set_query_inputs("200", "10"); - - tools.begin_resolve_with_backend(&backend); - - assert!(requested.borrow().is_empty()); - assert_eq!( - tools.status_message(), - Some("latitude must be between -90 and 90") - ); - assert!(!tools.is_pending()); - } - - #[test] - fn apply_result_keeps_up_to_three_matches_available() { - let mut tools = ReverseLookupTools::default(); - tools.lookup_state = HomeLocationLookupState::Pending { - queried_point: RadrootsLocationPoint { - lat: 1.25, - lng: -2.5, - }, - }; - - tools.apply_result(Ok(vec![ - sample_result(1, "one"), - sample_result(2, "two"), - sample_result(3, "three"), - ])); - - let result = tools.lookup_result().expect("lookup result"); - assert_eq!(result.matches.len(), 3); - assert_eq!(result.matches[0].name, "one"); - assert_eq!(result.matches[2].name, "three"); - } - - #[test] - fn apply_poll_error_sets_failed_status() { - let mut tools = ReverseLookupTools::default(); - - tools.apply_poll_error("background worker failed".to_owned()); - - assert_eq!(tools.status_message(), Some("background worker failed")); - } - - fn sample_result(id: i64, name: &str) -> RadrootsResolvedLocation { - RadrootsResolvedLocation { - id, - name: name.to_owned(), - admin1_id: None, - admin1_name: Some("state".to_owned()), - country_id: "US".to_owned(), - country_name: Some("United States".to_owned()), - point: RadrootsLocationPoint { lat: 1.0, lng: 2.0 }, - } - } -} diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs @@ -1,2742 +0,0 @@ -#![forbid(unsafe_code)] - -use eframe::egui; -use std::time::{Duration, Instant}; -use zeroize::Zeroizing; - -mod account_roster; -mod home_location_tools; -mod location_resolver; -mod offline_geocoder; -mod remote_signer; -mod secret_keys; -mod storage_paths; - -pub const APP_NAME: &str = "Rad Roots"; - -pub use account_roster::{RadrootsAccountCustody, RadrootsAccountSummary}; -pub use location_resolver::{ - RadrootsLocationCountry, RadrootsLocationCountryCenterLookupResult, - RadrootsLocationCountryListResult, RadrootsLocationPoint, RadrootsLocationResolverError, - RadrootsLocationReverseOptions, RadrootsResolvedLocation, RadrootsReverseLocationLookupResult, -}; -pub use offline_geocoder::{ - RadrootsOfflineGeocoderDiagnostic, RadrootsOfflineGeocoderPlatform, - RadrootsOfflineGeocoderState, RadrootsOfflineGeocoderUnavailableKind, -}; -pub use remote_signer::{ - RadrootsPendingRemoteSignerConnection, RadrootsRemoteSignerPreview, - RadrootsRemoteSignerSignedNote, -}; -pub use secret_keys::{RadrootsSecretImportMode, RadrootsSecretImportRequest}; -pub use storage_paths::{ - RadrootsAppStorageLayout, interactive_user_app_storage_layout_with_resolver, - mobile_native_app_storage_layout, -}; - -use home_location_tools::HomeLocationTools; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SetupActionState { - pub label: String, - pub enabled: bool, - pub pending: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ImportActionState { - pub label: String, - pub enabled: bool, - pub pending: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PasteActionState { - pub label: String, - pub enabled: bool, - pub pending: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct HomeActionState { - pub kind: HomeActionKind, - pub label: String, - pub enabled: bool, - pub pending: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HomeActionKind { - BackupSecretKey, - RevealRawSecretKey, - RemoveLocalKey, - ResetDevice, - DisconnectSigner, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum HomeActionResult { - None, - IdentityState(IdentityGateState), - RevealEncryptedSecretKey { ncryptsec: String }, - RevealRawSecretKey { nsec: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IdentityGateState { - Missing, - Ready { account_id: String }, - Unsupported { reason: String }, -} - -pub trait RadrootsAppBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String>; - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - Ok(Vec::new()) - } - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - None - } - fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - Ok(None) - } - fn setup_action_state(&self) -> SetupActionState; - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String>; - fn home_setup_action_state(&self) -> Option<SetupActionState> { - None - } - fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - fn import_action_state(&self) -> Option<ImportActionState> { - None - } - fn request_import_action( - &self, - _request: &RadrootsSecretImportRequest, - ) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - fn import_paste_action_state(&self) -> Option<PasteActionState> { - None - } - fn request_import_paste_action(&self) -> Result<Option<String>, String> { - Ok(None) - } - fn remote_signer_action_state(&self) -> Option<SetupActionState> { - None - } - fn preview_remote_signer_connection( - &self, - _input: &str, - ) -> Result<RadrootsRemoteSignerPreview, String> { - Err("remote signer onboarding is not available in this build".to_owned()) - } - fn request_remote_signer_connection( - &self, - _input: &str, - ) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - fn pending_remote_signer_connection( - &self, - ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> { - Ok(None) - } - fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> { - Ok(()) - } - fn remote_signer_note_action_state(&self) -> Option<SetupActionState> { - None - } - fn selected_remote_signer_approved_permissions(&self) -> Option<Vec<String>> { - None - } - fn request_remote_signer_note_action(&self, _content: &str) -> Result<(), String> { - Ok(()) - } - fn poll_remote_signer_note_action_result( - &self, - ) -> Result<Option<RadrootsRemoteSignerSignedNote>, String> { - Ok(None) - } - fn home_action_states(&self) -> Vec<HomeActionState> { - Vec::new() - } - fn request_home_action(&self, _action: HomeActionKind) -> Result<HomeActionResult, String> { - Ok(HomeActionResult::None) - } - fn request_secret_key_backup_action( - &self, - _password: &str, - ) -> Result<HomeActionResult, String> { - Ok(HomeActionResult::None) - } - fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> { - Ok(None) - } - fn request_select_account( - &self, - _account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - Ok(None) - } - fn reverse_location( - &self, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - fn request_reverse_location_lookup( - &self, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - Ok(None) - } - fn request_location_country_list(&self) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - fn poll_location_country_list_result( - &self, - ) -> Result<Option<RadrootsLocationCountryListResult>, String> { - Ok(None) - } - fn request_location_country_center_lookup( - &self, - _country_id: &str, - ) -> Result<(), RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - fn poll_location_country_center_lookup_result( - &self, - ) -> Result<Option<RadrootsLocationCountryCenterLookupResult>, String> { - Ok(None) - } - fn list_location_countries( - &self, - ) -> Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } - fn location_country_center( - &self, - _country_id: &str, - ) -> Result<RadrootsLocationPoint, RadrootsLocationResolverError> { - Err(RadrootsLocationResolverError::Unsupported) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum AppScreen { - Setup, - Home { account_id: String }, -} - -const RAW_SECRET_REVEAL_TIMEOUT: Duration = Duration::from_secs(30); - -#[derive(Debug, Clone, PartialEq, Eq)] -enum RevealedSecretMaterial { - EncryptedSecretKey(Zeroizing<String>), - RawSecretKey { - nsec: Zeroizing<String>, - revealed_at: Instant, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum RemoteSignerEntryState { - Closed, - Editing, - Review(RadrootsRemoteSignerPreview), - WaitingApproval(RadrootsPendingRemoteSignerConnection), -} - -impl RevealedSecretMaterial { - fn label(&self) -> &'static str { - match self { - Self::EncryptedSecretKey(_) => "Encrypted Secret Key", - Self::RawSecretKey { .. } => "Raw Secret Key", - } - } - - fn value(&self) -> &str { - match self { - Self::EncryptedSecretKey(ncryptsec) => ncryptsec.as_str(), - Self::RawSecretKey { nsec, .. } => nsec.as_str(), - } - } - - fn dismiss_label(&self) -> &'static str { - match self { - Self::EncryptedSecretKey(_) => "Dismiss Encrypted Secret Key", - Self::RawSecretKey { .. } => "Dismiss Raw Secret Key", - } - } - - fn is_raw(&self) -> bool { - matches!(self, Self::RawSecretKey { .. }) - } - - fn raw_secret_expired(&self) -> bool { - match self { - Self::RawSecretKey { revealed_at, .. } => { - revealed_at.elapsed() >= RAW_SECRET_REVEAL_TIMEOUT - } - Self::EncryptedSecretKey(_) => false, - } - } -} - -pub struct RadrootsApp { - backend: Box<dyn RadrootsAppBackend>, - screen: AppScreen, - account_roster: Vec<RadrootsAccountSummary>, - offline_geocoder_state: Option<RadrootsOfflineGeocoderState>, - status_message: Option<String>, - home_location_tools: HomeLocationTools, - pending_home_confirmation: Option<HomeActionKind>, - pending_import_mode: Option<RadrootsSecretImportMode>, - remote_signer_entry_state: RemoteSignerEntryState, - remote_signer_input: Zeroizing<String>, - secret_key_input: Zeroizing<String>, - import_password_input: Zeroizing<String>, - pending_secret_key_backup_entry: bool, - secret_key_backup_password_input: Zeroizing<String>, - secret_key_backup_password_confirm_input: Zeroizing<String>, - remote_signer_note_input: Zeroizing<String>, - revealed_secret_material: Option<RevealedSecretMaterial>, -} - -impl RadrootsApp { - fn clear_secret_import_entry(&mut self) { - self.pending_import_mode = None; - self.secret_key_input.clear(); - self.import_password_input.clear(); - } - - fn clear_secret_key_backup_entry(&mut self) { - self.pending_secret_key_backup_entry = false; - self.secret_key_backup_password_input.clear(); - self.secret_key_backup_password_confirm_input.clear(); - } - - fn clear_revealed_secret_material(&mut self) { - self.revealed_secret_material = None; - } - - fn clear_remote_signer_entry(&mut self) { - self.remote_signer_entry_state = RemoteSignerEntryState::Closed; - self.remote_signer_input.clear(); - } - - fn clear_secret_key_ui_state(&mut self) { - self.clear_remote_signer_entry(); - self.clear_secret_import_entry(); - self.clear_secret_key_backup_entry(); - self.clear_revealed_secret_material(); - } - - fn open_import_entry(&mut self) { - self.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey); - self.secret_key_input.clear(); - self.import_password_input.clear(); - self.status_message = None; - } - - fn import_mode(&self) -> RadrootsSecretImportMode { - self.pending_import_mode.unwrap_or_default() - } - - fn set_import_mode(&mut self, mode: RadrootsSecretImportMode) { - self.pending_import_mode = Some(mode); - self.secret_key_input.clear(); - self.import_password_input.clear(); - self.status_message = None; - } - - fn secret_import_request(&self) -> Result<RadrootsSecretImportRequest, String> { - let mode = self.import_mode(); - let secret_text = self.secret_key_input.trim().to_owned(); - if secret_text.is_empty() { - return Err(match mode { - RadrootsSecretImportMode::EncryptedSecretKey => { - "enter an encrypted secret key to continue".to_owned() - } - RadrootsSecretImportMode::RawSecretKey => { - "enter a raw secret key to continue".to_owned() - } - }); - } - - let password = if mode.requires_password() { - if self.import_password_input.is_empty() { - return Err("enter a password to import the encrypted secret key".to_owned()); - } - Some(self.import_password_input.to_string()) - } else { - None - }; - - Ok(RadrootsSecretImportRequest { - mode, - secret_text, - password, - }) - } - - fn request_secret_key_backup_action(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - - if self.secret_key_backup_password_input.is_empty() { - self.status_message = - Some("enter a password to create an encrypted secret key backup".to_owned()); - return; - } - - if self.secret_key_backup_password_input != self.secret_key_backup_password_confirm_input { - self.status_message = Some("backup passwords do not match".to_owned()); - return; - } - - match self - .backend - .request_secret_key_backup_action(self.secret_key_backup_password_input.as_str()) - { - Ok(result) => { - self.clear_secret_key_backup_entry(); - self.apply_home_action_result(result); - } - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn sync_revealed_secret_material_lifetime(&mut self) { - if self - .revealed_secret_material - .as_ref() - .is_some_and(RevealedSecretMaterial::raw_secret_expired) - { - self.clear_revealed_secret_material(); - } - } - - fn clear_raw_secret_when_app_unfocused(&mut self, ctx: &egui::Context) { - if self - .revealed_secret_material - .as_ref() - .is_some_and(RevealedSecretMaterial::is_raw) - && ctx.input(|input| input.viewport().focused == Some(false)) - { - self.clear_revealed_secret_material(); - } - } - - pub fn new(backend: Box<dyn RadrootsAppBackend>) -> Self { - let mut app = Self { - backend, - screen: AppScreen::Setup, - account_roster: Vec::new(), - offline_geocoder_state: None, - status_message: None, - home_location_tools: HomeLocationTools::new(), - pending_home_confirmation: None, - pending_import_mode: None, - remote_signer_entry_state: RemoteSignerEntryState::Closed, - remote_signer_input: Zeroizing::new(String::new()), - secret_key_input: Zeroizing::new(String::new()), - import_password_input: Zeroizing::new(String::new()), - pending_secret_key_backup_entry: false, - secret_key_backup_password_input: Zeroizing::new(String::new()), - secret_key_backup_password_confirm_input: Zeroizing::new(String::new()), - remote_signer_note_input: Zeroizing::new(String::new()), - revealed_secret_material: None, - }; - app.offline_geocoder_state = app.backend.offline_geocoder_state(); - match app.backend.load_identity_state() { - Ok(state) => app.apply_identity_state(state), - Err(err) => { - app.screen = AppScreen::Setup; - app.status_message = Some(err); - } - } - app.sync_remote_signer_entry_from_backend(); - app - } - - fn refresh_account_roster(&mut self) { - match self.backend.load_account_roster() { - Ok(account_roster) => { - self.account_roster = account_roster; - } - Err(err) => { - self.account_roster.clear(); - self.status_message = Some(err); - } - } - } - - fn apply_identity_state(&mut self, state: IdentityGateState) { - match state { - IdentityGateState::Missing => { - self.screen = AppScreen::Setup; - self.account_roster.clear(); - self.status_message = None; - self.home_location_tools.clear(); - self.pending_home_confirmation = None; - self.clear_secret_key_ui_state(); - } - IdentityGateState::Ready { account_id } => { - self.screen = AppScreen::Home { account_id }; - self.status_message = None; - self.refresh_account_roster(); - self.home_location_tools.clear(); - self.pending_home_confirmation = None; - self.clear_secret_key_ui_state(); - } - IdentityGateState::Unsupported { reason } => { - self.screen = AppScreen::Setup; - self.account_roster.clear(); - self.status_message = Some(reason); - self.home_location_tools.clear(); - self.pending_home_confirmation = None; - self.clear_secret_key_ui_state(); - } - } - } - - fn request_setup_action(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - match self.backend.request_setup_action() { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_home_setup_action(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - self.pending_home_confirmation = None; - self.clear_remote_signer_entry(); - self.clear_secret_import_entry(); - match self.backend.request_home_setup_action() { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => self.refresh_account_roster(), - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_import_action(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - let request = match self.secret_import_request() { - Ok(request) => request, - Err(err) => { - self.status_message = Some(err); - return; - } - }; - match self.backend.request_import_action(&request) { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_import_paste_action(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - match self.backend.request_import_paste_action() { - Ok(Some(secret_key)) => { - self.secret_key_input = Zeroizing::new(secret_key); - } - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn open_remote_signer_entry(&mut self) { - self.remote_signer_entry_state = RemoteSignerEntryState::Editing; - self.remote_signer_input.clear(); - self.status_message = None; - } - - fn sync_remote_signer_entry_from_backend(&mut self) { - match self.backend.pending_remote_signer_connection() { - Ok(Some(pending)) => { - if !matches!( - self.remote_signer_entry_state, - RemoteSignerEntryState::Editing | RemoteSignerEntryState::Review(_) - ) { - self.remote_signer_entry_state = - RemoteSignerEntryState::WaitingApproval(pending); - } - } - Ok(None) => { - if matches!( - self.remote_signer_entry_state, - RemoteSignerEntryState::WaitingApproval(_) - ) { - self.clear_remote_signer_entry(); - } - } - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_remote_signer_preview(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - match self - .backend - .preview_remote_signer_connection(self.remote_signer_input.as_str()) - { - Ok(preview) => { - self.remote_signer_entry_state = RemoteSignerEntryState::Review(preview); - } - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_remote_signer_connect(&mut self) { - self.status_message = None; - self.clear_revealed_secret_material(); - let pending_summary = match &self.remote_signer_entry_state { - RemoteSignerEntryState::Review(preview) => preview.pending_summary(), - _ => { - self.status_message = - Some("review the remote signer details before connecting".to_owned()); - return; - } - }; - match self - .backend - .request_remote_signer_connection(self.remote_signer_input.as_str()) - { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => { - self.remote_signer_entry_state = - RemoteSignerEntryState::WaitingApproval(pending_summary); - self.sync_remote_signer_entry_from_backend(); - } - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_cancel_pending_remote_signer(&mut self) { - self.status_message = None; - match self - .backend - .request_cancel_pending_remote_signer_connection() - { - Ok(()) => self.clear_remote_signer_entry(), - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_remote_signer_note_action(&mut self) { - self.status_message = None; - match self - .backend - .request_remote_signer_note_action(self.remote_signer_note_input.as_str()) - { - Ok(()) => {} - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_select_account(&mut self, account_id: &str) { - self.status_message = None; - self.clear_revealed_secret_material(); - self.pending_home_confirmation = None; - self.clear_secret_key_ui_state(); - match self.backend.request_select_account(account_id) { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => self.refresh_account_roster(), - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn request_home_action(&mut self, action: HomeActionKind) { - self.status_message = None; - self.clear_revealed_secret_material(); - match self.backend.request_home_action(action) { - Ok(result) => self.apply_home_action_result(result), - Err(err) => { - self.status_message = Some(err); - } - } - } - - fn apply_home_action_result(&mut self, result: HomeActionResult) { - match result { - HomeActionResult::IdentityState(state) => self.apply_identity_state(state), - HomeActionResult::RevealEncryptedSecretKey { ncryptsec } => { - self.revealed_secret_material = Some(RevealedSecretMaterial::EncryptedSecretKey( - Zeroizing::new(ncryptsec), - )); - self.pending_home_confirmation = None; - } - HomeActionResult::RevealRawSecretKey { nsec } => { - self.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey { - nsec: Zeroizing::new(nsec), - revealed_at: Instant::now(), - }); - self.pending_home_confirmation = None; - } - HomeActionResult::None => {} - } - } - - fn home_action_requires_confirmation(action: HomeActionKind) -> bool { - !matches!(action, HomeActionKind::BackupSecretKey) - } - - fn home_action_confirmation_message(action: HomeActionKind) -> &'static str { - match action { - HomeActionKind::BackupSecretKey => { - "This exports the current local secret key in encrypted form for backup." - } - HomeActionKind::RevealRawSecretKey => { - "This reveals the current local secret key in plaintext. Use encrypted backup instead when possible." - } - HomeActionKind::RemoveLocalKey => { - "This removes the current key from this device and returns the app to setup." - } - HomeActionKind::ResetDevice => { - "This removes all app-managed local identity state from this device and returns the app to setup." - } - HomeActionKind::DisconnectSigner => { - "This disconnects the current external signer from the app. It does not delete the signer key." - } - } - } - - fn sync_backend(&mut self) { - match self.backend.poll_offline_geocoder_state() { - Ok(Some(state)) => { - self.offline_geocoder_state = Some(state); - } - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - match self.backend.poll_home_action_result() { - Ok(Some(result)) => self.apply_home_action_result(result), - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - match self.backend.poll_remote_signer_note_action_result() { - Ok(Some(result)) => { - self.remote_signer_note_input.clear(); - self.status_message = Some(format!( - "Signed remote kind 1 note: {}", - result.event_id_hex - )); - } - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - match self.backend.poll_reverse_location_lookup_result() { - Ok(Some(result)) => self.home_location_tools.apply_reverse_lookup_result(result), - Ok(None) => {} - Err(err) => { - self.home_location_tools - .apply_reverse_lookup_poll_error(err); - } - } - match self.backend.poll_location_country_list_result() { - Ok(Some(result)) => self.home_location_tools.apply_country_list_result(result), - Ok(None) => {} - Err(err) => { - self.home_location_tools.apply_country_list_poll_error(err); - } - } - match self.backend.poll_location_country_center_lookup_result() { - Ok(Some(result)) => self.home_location_tools.apply_country_center_result(result), - Ok(None) => {} - Err(err) => { - self.home_location_tools - .apply_country_center_poll_error(err); - } - } - match self.backend.poll_identity_state() { - Ok(Some(state)) => self.apply_identity_state(state), - Ok(None) => {} - Err(err) => { - self.status_message = Some(err); - } - } - self.sync_remote_signer_entry_from_backend(); - } - - fn render_import_entry( - &mut self, - ui: &mut egui::Ui, - import_action: &ImportActionState, - import_paste_action: Option<&PasteActionState>, - ) { - let import_mode = self.import_mode(); - ui.vertical_centered(|ui| { - ui.set_max_width(ui.available_width().min(560.0)); - ui.label(import_mode.helper_text()); - ui.add_space(8.0); - if ui.button(import_mode.switch_label()).clicked() { - self.set_import_mode(import_mode.toggle()); - } - ui.add_space(8.0); - ui.add( - egui::TextEdit::singleline(&mut *self.secret_key_input) - .hint_text(import_mode.hint_text()) - .desired_width(ui.available_width()), - ); - if import_mode.requires_password() { - ui.add_space(8.0); - ui.add( - egui::TextEdit::singleline(&mut *self.import_password_input) - .password(true) - .hint_text("Enter Backup Password") - .desired_width(ui.available_width()), - ); - } - ui.add_space(8.0); - if let Some(import_paste_action) = import_paste_action { - let paste_clicked = ui - .add_enabled( - import_paste_action.enabled, - egui::Button::new(import_paste_action.label.clone()), - ) - .clicked(); - if paste_clicked { - self.request_import_paste_action(); - } - ui.add_space(8.0); - } - ui.horizontal_centered(|ui| { - let confirm_clicked = ui - .add_enabled( - import_action.enabled, - egui::Button::new(import_action.label.clone()), - ) - .clicked(); - if confirm_clicked { - self.request_import_action(); - } - - if ui.button("Cancel").clicked() { - self.clear_secret_import_entry(); - self.status_message = None; - } - }); - }); - } - - fn render_secret_key_backup_entry(&mut self, ui: &mut egui::Ui, action: &HomeActionState) { - ui.vertical_centered(|ui| { - ui.set_max_width(ui.available_width().min(560.0)); - ui.label("Create an encrypted backup of the current local secret key."); - ui.add_space(8.0); - ui.add( - egui::TextEdit::singleline(&mut *self.secret_key_backup_password_input) - .password(true) - .hint_text("Enter Backup Password") - .desired_width(ui.available_width()), - ); - ui.add_space(8.0); - ui.add( - egui::TextEdit::singleline(&mut *self.secret_key_backup_password_confirm_input) - .password(true) - .hint_text("Confirm Backup Password") - .desired_width(ui.available_width()), - ); - ui.add_space(8.0); - ui.horizontal_centered(|ui| { - let confirm_clicked = ui - .add_enabled(action.enabled, egui::Button::new(action.label.clone())) - .clicked(); - if confirm_clicked { - self.request_secret_key_backup_action(); - } - - if ui.button("Cancel").clicked() { - self.clear_secret_key_backup_entry(); - self.status_message = None; - } - }); - }); - } - - fn render_remote_signer_entry(&mut self, ui: &mut egui::Ui, action: &SetupActionState) { - ui.vertical_centered(|ui| { - ui.set_max_width(ui.available_width().min(560.0)); - match &self.remote_signer_entry_state { - RemoteSignerEntryState::Closed => {} - RemoteSignerEntryState::Editing => { - ui.label( - "Connect an approved remote signer using its bunker uri or discovery url.", - ); - ui.add_space(8.0); - ui.add( - egui::TextEdit::singleline(&mut *self.remote_signer_input) - .hint_text("bunker://... or http://localhost/connect?uri=...") - .desired_width(ui.available_width()), - ); - ui.add_space(8.0); - ui.horizontal_centered(|ui| { - if ui - .add_enabled(action.enabled, egui::Button::new("Review Remote Signer")) - .clicked() - { - self.request_remote_signer_preview(); - } - if ui.button("Cancel").clicked() { - self.clear_remote_signer_entry(); - self.status_message = None; - } - }); - } - RemoteSignerEntryState::Review(preview) => { - ui.label("Review the remote signer before connecting."); - ui.add_space(8.0); - ui.monospace(format!("source: {}", preview.source_label)); - ui.monospace(format!("signer: {}", preview.signer_npub)); - if preview.relays.is_empty() { - ui.label("No relays were provided by this signer."); - } else { - ui.label("Relays"); - for relay in &preview.relays { - ui.monospace(relay); - } - } - ui.add_space(8.0); - if preview.requested_permissions.is_empty() { - ui.label("No additional permissions are requested in this slice."); - } else { - ui.label("Requested permissions"); - for permission in &preview.requested_permissions { - ui.monospace(permission); - } - } - ui.add_space(8.0); - ui.horizontal_centered(|ui| { - if ui - .add_enabled(action.enabled, egui::Button::new(action.label.clone())) - .clicked() - { - self.request_remote_signer_connect(); - } - if ui.button("Cancel").clicked() { - self.clear_remote_signer_entry(); - self.status_message = None; - } - }); - } - RemoteSignerEntryState::WaitingApproval(pending) => { - ui.label(action.label.as_str()); - if pending.auth_url.is_some() { - ui.add_space(8.0); - ui.label( - "Authorize the remote signer in the browser, then keep this screen open while the app waits for the replayed response.", - ); - } else if action.label == "Remote Signer Approval Check Retrying" { - ui.add_space(8.0); - ui.label( - "The app is retrying approval checks after a relay or network failure.", - ); - } else { - ui.add_space(8.0); - ui.label("Remote signer connection is waiting for signer approval."); - } - ui.add_space(8.0); - ui.monospace(format!("signer: {}", pending.signer_npub)); - if pending.relays.is_empty() { - ui.label("No relays were provided by this signer."); - } else { - ui.label("Relays"); - for relay in &pending.relays { - ui.monospace(relay); - } - } - if let Some(auth_url) = &pending.auth_url { - ui.add_space(8.0); - ui.label("Authorization url"); - ui.monospace(auth_url); - } - ui.add_space(8.0); - if ui.button("Cancel Pending Remote Signer").clicked() { - self.request_cancel_pending_remote_signer(); - } - } - } - }); - } - - fn render_home_account_section(&mut self, ui: &mut egui::Ui) { - let AppScreen::Home { account_id } = &self.screen else { - return; - }; - let selected_account_id = account_id.clone(); - let selected_summary = self - .account_roster - .iter() - .find(|account| account.account_id == selected_account_id) - .cloned(); - - ui.label("home"); - ui.add_space(8.0); - ui.label("A signing identity is configured."); - ui.add_space(12.0); - - if let Some(summary) = selected_summary { - ui.label(summary.display_label()); - ui.monospace(format!("account id: {}", summary.account_id)); - ui.monospace(format!("npub: {}", summary.npub)); - ui.monospace(format!("custody: {}", summary.custody.label())); - if summary.custody == RadrootsAccountCustody::RemoteSigner { - if let Some(note_action) = self.backend.remote_signer_note_action_state() { - if note_action.pending { - ui.ctx().request_repaint(); - } - ui.add_space(16.0); - ui.label("Remote signer note"); - if let Some(permissions) = - self.backend.selected_remote_signer_approved_permissions() - { - ui.add_space(8.0); - if permissions.is_empty() { - ui.label("Approved permissions: none"); - } else { - ui.label("Approved permissions"); - for permission in permissions { - ui.monospace(permission); - } - } - } - ui.add_space(8.0); - ui.add( - egui::TextEdit::multiline(&mut *self.remote_signer_note_input) - .hint_text("Write a kind 1 note to sign through the remote signer") - .desired_rows(3) - .desired_width(ui.available_width().min(560.0)), - ); - ui.add_space(8.0); - if ui - .add_enabled(note_action.enabled, egui::Button::new(note_action.label)) - .clicked() - { - self.request_remote_signer_note_action(); - } - } - } - } else { - ui.label("Selected account details are unavailable."); - ui.monospace(format!("account id: {selected_account_id}")); - } - - if !self.account_roster.is_empty() { - ui.add_space(16.0); - ui.label("Accounts"); - let mut next_selected_account_id = None; - for account in &self.account_roster { - ui.add_space(8.0); - ui.horizontal_wrapped(|ui| { - let is_selected = account.account_id == selected_account_id; - ui.label(account.display_label()); - ui.monospace(account.npub.as_str()); - ui.monospace(account.custody.label()); - if is_selected { - ui.label("selected"); - } else if ui.button("Select Account").clicked() { - next_selected_account_id = Some(account.account_id.clone()); - } - }); - } - if let Some(account_id) = next_selected_account_id { - self.request_select_account(account_id.as_str()); - } - } - - let home_setup_action = self.backend.home_setup_action_state(); - let import_action = self.backend.import_action_state(); - let import_paste_action = self.backend.import_paste_action_state(); - let remote_signer_action = self.backend.remote_signer_action_state(); - if home_setup_action.is_some() || import_action.is_some() || remote_signer_action.is_some() - { - ui.add_space(16.0); - ui.label("Add account"); - } - - if let Some(home_setup_action) = home_setup_action { - if home_setup_action.pending { - ui.ctx().request_repaint(); - } - ui.add_space(8.0); - if ui - .add_enabled( - home_setup_action.enabled, - egui::Button::new(home_setup_action.label), - ) - .clicked() - { - self.request_home_setup_action(); - } - } - - if let Some(import_action) = import_action { - if import_action.pending { - ui.ctx().request_repaint(); - } - if let Some(import_paste_action) = &import_paste_action { - if import_paste_action.pending { - ui.ctx().request_repaint(); - } - } - ui.add_space(8.0); - if self.pending_import_mode.is_some() { - self.render_import_entry(ui, &import_action, import_paste_action.as_ref()); - } else if ui.button(import_action.label).clicked() { - self.open_import_entry(); - } - } - - if let Some(remote_signer_action) = remote_signer_action { - if remote_signer_action.pending { - ui.ctx().request_repaint(); - } - ui.add_space(8.0); - if matches!( - self.remote_signer_entry_state, - RemoteSignerEntryState::Closed - ) { - if ui - .add_enabled( - remote_signer_action.enabled, - egui::Button::new(remote_signer_action.label), - ) - .clicked() - { - self.open_remote_signer_entry(); - } - } else { - self.render_remote_signer_entry(ui, &remote_signer_action); - } - } - } - - fn render_offline_geocoder_status(&self, ui: &mut egui::Ui) { - let Some(state) = &self.offline_geocoder_state else { - return; - }; - - ui.add_space(16.0); - ui.label(state.summary_label()); - - if let Some(user_message) = state.user_message() { - ui.add_space(6.0); - ui.label(user_message); - ui.add_space(6.0); - ui.collapsing("Offline geocoder details", |ui| { - if let Some(diagnostic) = state.diagnostic() { - ui.label(diagnostic.technical_message); - ui.add_space(6.0); - ui.monospace(format!("platform: {}", diagnostic.platform_code)); - ui.monospace(format!( - "asset revision: {}", - diagnostic.asset_revision.as_deref().unwrap_or("unknown") - )); - ui.monospace(format!("diagnostic code: {}", diagnostic.code)); - if ui.button("Copy Offline Geocoder Diagnostic").clicked() { - ui.ctx().copy_text(diagnostic.export_text()); - } - } - if cfg!(debug_assertions) { - if let Some(debug_message) = state.debug_message() { - ui.add_space(6.0); - ui.monospace(debug_message); - } - } - }); - } - } -} - -impl eframe::App for RadrootsApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.sync_backend(); - self.sync_revealed_secret_material_lifetime(); - self.clear_raw_secret_when_app_unfocused(ctx); - if matches!( - self.offline_geocoder_state, - Some(RadrootsOfflineGeocoderState::Initializing) - ) { - ctx.request_repaint_after(Duration::from_millis(100)); - } - if self.home_location_tools.is_pending() { - ctx.request_repaint_after(Duration::from_millis(100)); - } - if self - .revealed_secret_material - .as_ref() - .is_some_and(RevealedSecretMaterial::is_raw) - { - ctx.request_repaint_after(Duration::from_millis(200)); - } - - egui::CentralPanel::default().show(ctx, |ui| { - ui.vertical_centered(|ui| { - ui.add_space(48.0); - ui.heading(APP_NAME); - ui.add_space(12.0); - - match self.screen.clone() { - AppScreen::Setup => { - let action = self.backend.setup_action_state(); - if action.pending { - ctx.request_repaint(); - } - let import_action = self.backend.import_action_state(); - if let Some(import_action) = &import_action { - if import_action.pending { - ctx.request_repaint(); - } - } - let import_paste_action = self.backend.import_paste_action_state(); - if let Some(import_paste_action) = &import_paste_action { - if import_paste_action.pending { - ctx.request_repaint(); - } - } - let remote_signer_action = self.backend.remote_signer_action_state(); - if let Some(remote_signer_action) = &remote_signer_action { - if remote_signer_action.pending { - ctx.request_repaint(); - } - } - - ui.label("setup"); - ui.add_space(8.0); - ui.label("A signing identity is required before the app can continue."); - ui.add_space(16.0); - let clicked = ui - .add_enabled(action.enabled, egui::Button::new(action.label)) - .clicked(); - if clicked { - self.request_setup_action(); - } - - if let Some(import_action) = import_action { - ui.add_space(12.0); - if self.pending_import_mode.is_some() { - self.render_import_entry( - ui, - &import_action, - import_paste_action.as_ref(), - ); - } else if ui.button(import_action.label).clicked() { - self.open_import_entry(); - } - } - - if let Some(remote_signer_action) = remote_signer_action { - ui.add_space(12.0); - if matches!(self.remote_signer_entry_state, RemoteSignerEntryState::Closed) - { - if ui - .add_enabled( - remote_signer_action.enabled, - egui::Button::new(remote_signer_action.label), - ) - .clicked() - { - self.open_remote_signer_entry(); - } - } else { - self.render_remote_signer_entry(ui, &remote_signer_action); - } - } - } - AppScreen::Home { .. } => { - self.render_home_account_section(ui); - self.home_location_tools.render( - ui, - self.backend.as_ref(), - self.offline_geocoder_state.as_ref(), - ); - - let actions = self.backend.home_action_states(); - for (index, action) in actions.into_iter().enumerate() { - ui.add_space(if index == 0 { 20.0 } else { 12.0 }); - if action.pending { - ctx.request_repaint(); - } - - if action.kind == HomeActionKind::BackupSecretKey - && self.pending_secret_key_backup_entry - { - self.render_secret_key_backup_entry(ui, &action); - } else if action.kind == HomeActionKind::BackupSecretKey - && ui - .add_enabled( - action.enabled, - egui::Button::new(action.label.clone()), - ) - .clicked() - { - self.pending_secret_key_backup_entry = true; - self.secret_key_backup_password_input.clear(); - self.secret_key_backup_password_confirm_input.clear(); - self.status_message = None; - } else if Self::home_action_requires_confirmation(action.kind) - && self.pending_home_confirmation == Some(action.kind) - { - ui.vertical_centered(|ui| { - ui.set_max_width(ui.available_width().min(560.0)); - ui.label(Self::home_action_confirmation_message(action.kind)); - ui.add_space(8.0); - ui.horizontal_centered(|ui| { - let confirm_clicked = ui - .add_enabled( - action.enabled, - egui::Button::new(action.label.clone()), - ) - .clicked(); - if confirm_clicked { - self.request_home_action(action.kind); - } - - if ui.button("Cancel").clicked() { - self.pending_home_confirmation = None; - self.status_message = None; - } - }); - }); - } else if Self::home_action_requires_confirmation(action.kind) - && self.pending_home_confirmation.is_none() - && ui.button(action.label.clone()).clicked() - { - self.pending_home_confirmation = Some(action.kind); - } else if !Self::home_action_requires_confirmation(action.kind) - && ui - .add_enabled( - action.enabled, - egui::Button::new(action.label.clone()), - ) - .clicked() - { - self.request_home_action(action.kind); - } - } - - if let Some((label, value, dismiss_label, is_raw)) = - self.revealed_secret_material.as_ref().map(|material| { - ( - material.label(), - material.value().to_owned(), - material.dismiss_label(), - material.is_raw(), - ) - }) - { - ui.add_space(20.0); - ui.label(label); - ui.add_space(8.0); - ui.monospace(value); - if is_raw { - ui.add_space(8.0); - ui.label( - "Raw secret reveal clears automatically after 30 seconds or when the app loses focus.", - ); - } - ui.add_space(8.0); - if ui.button(dismiss_label).clicked() { - self.clear_revealed_secret_material(); - } - } - } - } - - if let Some(message) = &self.status_message { - ui.add_space(16.0); - ui.label(message); - } - - self.render_offline_geocoder_status(ui); - }); - }); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{ - FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD, FIXTURE_BOB, fixture_identity_ncryptsec, - }; - use std::cell::RefCell; - use std::collections::VecDeque; - use std::rc::Rc; - - #[derive(Clone)] - struct MockBackend { - load: Result<IdentityGateState, String>, - account_roster: Rc<RefCell<Vec<RadrootsAccountSummary>>>, - offline_geocoder_state: Rc<RefCell<Option<RadrootsOfflineGeocoderState>>>, - offline_geocoder_poll: - Rc<RefCell<VecDeque<Result<Option<RadrootsOfflineGeocoderState>, String>>>>, - action_state: Rc<RefCell<SetupActionState>>, - home_setup_action_state: Rc<RefCell<Option<SetupActionState>>>, - import_action_state: Rc<RefCell<Option<ImportActionState>>>, - import_paste_action_state: Rc<RefCell<Option<PasteActionState>>>, - remote_signer_action_state: Rc<RefCell<Option<SetupActionState>>>, - remote_signer_preview: Rc<RefCell<VecDeque<Result<RadrootsRemoteSignerPreview, String>>>>, - remote_signer_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>, - pending_remote_signer: Rc<RefCell<Option<RadrootsPendingRemoteSignerConnection>>>, - cancel_pending_remote_signer: Rc<RefCell<VecDeque<Result<(), String>>>>, - remote_signer_note_action_state: Rc<RefCell<Option<SetupActionState>>>, - remote_signer_note_request: Rc<RefCell<VecDeque<Result<(), String>>>>, - remote_signer_note_poll: - Rc<RefCell<VecDeque<Result<Option<RadrootsRemoteSignerSignedNote>, String>>>>, - home_action_states: Rc<RefCell<Vec<HomeActionState>>>, - request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>, - home_setup_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>, - import_request: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>, - import_paste_request: Rc<RefCell<VecDeque<Result<Option<String>, String>>>>, - secret_key_backup_request: Rc<RefCell<VecDeque<Result<HomeActionResult, String>>>>, - home_request: Rc<RefCell<VecDeque<(HomeActionKind, Result<HomeActionResult, String>)>>>, - home_poll: Rc<RefCell<VecDeque<Result<Option<HomeActionResult>, String>>>>, - reverse_lookup_request: Rc<RefCell<VecDeque<Result<(), RadrootsLocationResolverError>>>>, - reverse_lookup_poll: - Rc<RefCell<VecDeque<Result<Option<RadrootsReverseLocationLookupResult>, String>>>>, - select_account_request: - Rc<RefCell<VecDeque<(String, Result<Option<IdentityGateState>, String>)>>>, - poll: Rc<RefCell<VecDeque<Result<Option<IdentityGateState>, String>>>>, - } - - impl MockBackend { - fn new( - load: Result<IdentityGateState, String>, - request: Vec<Result<Option<IdentityGateState>, String>>, - poll: Vec<Result<Option<IdentityGateState>, String>>, - action_state: SetupActionState, - ) -> Self { - Self { - load, - account_roster: Rc::new(RefCell::new(Vec::new())), - offline_geocoder_state: Rc::new(RefCell::new(None)), - offline_geocoder_poll: Rc::new(RefCell::new(VecDeque::new())), - action_state: Rc::new(RefCell::new(action_state)), - home_setup_action_state: Rc::new(RefCell::new(None)), - import_action_state: Rc::new(RefCell::new(None)), - import_paste_action_state: Rc::new(RefCell::new(None)), - remote_signer_action_state: Rc::new(RefCell::new(None)), - remote_signer_preview: Rc::new(RefCell::new(VecDeque::new())), - remote_signer_request: Rc::new(RefCell::new(VecDeque::new())), - pending_remote_signer: Rc::new(RefCell::new(None)), - cancel_pending_remote_signer: Rc::new(RefCell::new(VecDeque::new())), - remote_signer_note_action_state: Rc::new(RefCell::new(None)), - remote_signer_note_request: Rc::new(RefCell::new(VecDeque::new())), - remote_signer_note_poll: Rc::new(RefCell::new(VecDeque::new())), - home_action_states: Rc::new(RefCell::new(Vec::new())), - request: Rc::new(RefCell::new(request.into())), - home_setup_request: Rc::new(RefCell::new(VecDeque::new())), - import_request: Rc::new(RefCell::new(VecDeque::new())), - import_paste_request: Rc::new(RefCell::new(VecDeque::new())), - secret_key_backup_request: Rc::new(RefCell::new(VecDeque::new())), - home_request: Rc::new(RefCell::new(VecDeque::new())), - home_poll: Rc::new(RefCell::new(VecDeque::new())), - reverse_lookup_request: Rc::new(RefCell::new(VecDeque::new())), - reverse_lookup_poll: Rc::new(RefCell::new(VecDeque::new())), - select_account_request: Rc::new(RefCell::new(VecDeque::new())), - poll: Rc::new(RefCell::new(poll.into())), - } - } - - fn with_account_roster(self, account_roster: Vec<RadrootsAccountSummary>) -> Self { - *self.account_roster.borrow_mut() = account_roster; - self - } - - fn with_offline_geocoder_state( - self, - state: RadrootsOfflineGeocoderState, - poll: Vec<Result<Option<RadrootsOfflineGeocoderState>, String>>, - ) -> Self { - *self.offline_geocoder_state.borrow_mut() = Some(state); - self.offline_geocoder_poll.borrow_mut().extend(poll); - self - } - - fn with_import_action( - self, - action_state: ImportActionState, - request: Vec<Result<Option<IdentityGateState>, String>>, - ) -> Self { - *self.import_action_state.borrow_mut() = Some(action_state); - self.import_request.borrow_mut().extend(request); - self - } - - fn with_home_setup_action( - self, - action_state: SetupActionState, - request: Vec<Result<Option<IdentityGateState>, String>>, - ) -> Self { - *self.home_setup_action_state.borrow_mut() = Some(action_state); - self.home_setup_request.borrow_mut().extend(request); - self - } - - fn with_import_paste_action( - self, - action_state: PasteActionState, - request: Vec<Result<Option<String>, String>>, - ) -> Self { - *self.import_paste_action_state.borrow_mut() = Some(action_state); - self.import_paste_request.borrow_mut().extend(request); - self - } - - fn with_remote_signer_action(self, action_state: SetupActionState) -> Self { - *self.remote_signer_action_state.borrow_mut() = Some(action_state); - self - } - - fn with_remote_signer_preview( - self, - preview: Vec<Result<RadrootsRemoteSignerPreview, String>>, - ) -> Self { - self.remote_signer_preview.borrow_mut().extend(preview); - self - } - - fn with_remote_signer_request( - self, - request: Vec<Result<Option<IdentityGateState>, String>>, - ) -> Self { - self.remote_signer_request.borrow_mut().extend(request); - self - } - - fn with_pending_remote_signer( - self, - pending: Option<RadrootsPendingRemoteSignerConnection>, - ) -> Self { - *self.pending_remote_signer.borrow_mut() = pending; - self - } - - fn with_cancel_pending_remote_signer(self, request: Vec<Result<(), String>>) -> Self { - self.cancel_pending_remote_signer - .borrow_mut() - .extend(request); - self - } - - fn with_home_action( - self, - action_state: HomeActionState, - request: Vec<Result<HomeActionResult, String>>, - ) -> Self { - self.home_action_states - .borrow_mut() - .push(action_state.clone()); - self.home_request.borrow_mut().extend( - request - .into_iter() - .map(|result| (action_state.kind, result)), - ); - self - } - - fn with_secret_key_backup_request( - self, - request: Vec<Result<HomeActionResult, String>>, - ) -> Self { - self.secret_key_backup_request.borrow_mut().extend(request); - self - } - - fn with_home_action_poll( - self, - poll: Vec<Result<Option<HomeActionResult>, String>>, - ) -> Self { - self.home_poll.borrow_mut().extend(poll); - self - } - - fn with_reverse_lookup( - self, - request: Vec<Result<(), RadrootsLocationResolverError>>, - poll: Vec<Result<Option<RadrootsReverseLocationLookupResult>, String>>, - ) -> Self { - self.reverse_lookup_request.borrow_mut().extend(request); - self.reverse_lookup_poll.borrow_mut().extend(poll); - self - } - - fn with_select_account( - self, - account_id: &str, - request: Vec<Result<Option<IdentityGateState>, String>>, - ) -> Self { - self.select_account_request.borrow_mut().extend( - request - .into_iter() - .map(|result| (account_id.to_owned(), result)), - ); - self - } - } - - impl RadrootsAppBackend for MockBackend { - fn load_identity_state(&self) -> Result<IdentityGateState, String> { - self.load.clone() - } - - fn load_account_roster(&self) -> Result<Vec<RadrootsAccountSummary>, String> { - Ok(self.account_roster.borrow().clone()) - } - - fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { - self.offline_geocoder_state.borrow().clone() - } - - fn poll_offline_geocoder_state( - &self, - ) -> Result<Option<RadrootsOfflineGeocoderState>, String> { - self.offline_geocoder_poll - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn setup_action_state(&self) -> SetupActionState { - self.action_state.borrow().clone() - } - - fn request_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - self.request - .borrow_mut() - .pop_front() - .unwrap_or_else(|| Err("missing request response".into())) - } - - fn home_setup_action_state(&self) -> Option<SetupActionState> { - self.home_setup_action_state.borrow().clone() - } - - fn request_home_setup_action(&self) -> Result<Option<IdentityGateState>, String> { - self.home_setup_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn import_action_state(&self) -> Option<ImportActionState> { - self.import_action_state.borrow().clone() - } - - fn request_import_action( - &self, - _request: &RadrootsSecretImportRequest, - ) -> Result<Option<IdentityGateState>, String> { - self.import_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn request_secret_key_backup_action( - &self, - _password: &str, - ) -> Result<HomeActionResult, String> { - self.secret_key_backup_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(HomeActionResult::None)) - } - - fn import_paste_action_state(&self) -> Option<PasteActionState> { - self.import_paste_action_state.borrow().clone() - } - - fn request_import_paste_action(&self) -> Result<Option<String>, String> { - self.import_paste_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn remote_signer_action_state(&self) -> Option<SetupActionState> { - self.remote_signer_action_state.borrow().clone() - } - - fn preview_remote_signer_connection( - &self, - _input: &str, - ) -> Result<RadrootsRemoteSignerPreview, String> { - self.remote_signer_preview - .borrow_mut() - .pop_front() - .unwrap_or_else(|| Err("missing remote signer preview".into())) - } - - fn request_remote_signer_connection( - &self, - _input: &str, - ) -> Result<Option<IdentityGateState>, String> { - self.remote_signer_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn pending_remote_signer_connection( - &self, - ) -> Result<Option<RadrootsPendingRemoteSignerConnection>, String> { - Ok(self.pending_remote_signer.borrow().clone()) - } - - fn request_cancel_pending_remote_signer_connection(&self) -> Result<(), String> { - let result = self - .cancel_pending_remote_signer - .borrow_mut() - .pop_front() - .unwrap_or(Ok(())); - if result.is_ok() { - *self.pending_remote_signer.borrow_mut() = None; - } - result - } - - fn remote_signer_note_action_state(&self) -> Option<SetupActionState> { - self.remote_signer_note_action_state.borrow().clone() - } - - fn request_remote_signer_note_action(&self, _content: &str) -> Result<(), String> { - self.remote_signer_note_request - .borrow_mut() - .pop_front() - .unwrap_or(Ok(())) - } - - fn poll_remote_signer_note_action_result( - &self, - ) -> Result<Option<RadrootsRemoteSignerSignedNote>, String> { - self.remote_signer_note_poll - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn home_action_states(&self) -> Vec<HomeActionState> { - self.home_action_states.borrow().clone() - } - - fn request_home_action(&self, action: HomeActionKind) -> Result<HomeActionResult, String> { - let Some((expected_action, response)) = self.home_request.borrow_mut().pop_front() - else { - return Err("missing home action response".into()); - }; - if expected_action != action { - return Err(format!( - "unexpected home action request: expected {:?}, got {:?}", - expected_action, action - )); - } - response - } - - fn poll_home_action_result(&self) -> Result<Option<HomeActionResult>, String> { - self.home_poll.borrow_mut().pop_front().unwrap_or(Ok(None)) - } - - fn request_select_account( - &self, - account_id: &str, - ) -> Result<Option<IdentityGateState>, String> { - let Some((expected_account_id, response)) = - self.select_account_request.borrow_mut().pop_front() - else { - return Err("missing select-account response".into()); - }; - if expected_account_id != account_id { - return Err(format!( - "unexpected account selection request: expected {expected_account_id}, got {account_id}" - )); - } - response - } - - fn request_reverse_location_lookup( - &self, - _point: RadrootsLocationPoint, - _options: Option<RadrootsLocationReverseOptions>, - ) -> Result<(), RadrootsLocationResolverError> { - self.reverse_lookup_request - .borrow_mut() - .pop_front() - .unwrap_or(Err(RadrootsLocationResolverError::Unsupported)) - } - - fn poll_reverse_location_lookup_result( - &self, - ) -> Result<Option<RadrootsReverseLocationLookupResult>, String> { - self.reverse_lookup_poll - .borrow_mut() - .pop_front() - .unwrap_or(Ok(None)) - } - - fn poll_identity_state(&self) -> Result<Option<IdentityGateState>, String> { - self.poll.borrow_mut().pop_front().unwrap_or(Ok(None)) - } - } - - fn fixture_account_summary() -> RadrootsAccountSummary { - RadrootsAccountSummary { - account_id: FIXTURE_ALICE.account_id.into(), - npub: FIXTURE_ALICE.npub.into(), - label: Some("fixture alice".into()), - custody: RadrootsAccountCustody::LocalManaged, - } - } - - fn fixture_bob_account_summary() -> RadrootsAccountSummary { - RadrootsAccountSummary { - account_id: FIXTURE_BOB.account_id.into(), - npub: FIXTURE_BOB.npub.into(), - label: Some("fixture bob".into()), - custody: RadrootsAccountCustody::LocalManaged, - } - } - - fn fixture_ready_state() -> IdentityGateState { - IdentityGateState::Ready { - account_id: FIXTURE_ALICE.account_id.into(), - } - } - - fn fixture_home_screen() -> AppScreen { - AppScreen::Home { - account_id: FIXTURE_ALICE.account_id.into(), - } - } - - fn fixture_remote_signer_preview() -> RadrootsRemoteSignerPreview { - RadrootsRemoteSignerPreview { - source_label: "discovery url".into(), - signer_npub: FIXTURE_BOB.npub.into(), - relays: vec!["ws://localhost:8080".into()], - requested_permissions: vec!["sign_event:kind:1".into(), "switch_relays".into()], - } - } - - fn fixture_pending_remote_signer() -> RadrootsPendingRemoteSignerConnection { - fixture_remote_signer_preview().pending_summary() - } - - #[test] - fn startup_missing_key_enters_setup() { - let app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ))); - assert_eq!(app.screen, AppScreen::Setup); - assert_eq!(app.status_message, None); - } - - #[test] - fn startup_ready_key_enters_home() { - let app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ))); - assert_eq!(app.screen, fixture_home_screen()); - assert_eq!(app.status_message, None); - } - - #[test] - fn startup_ready_key_loads_account_roster() { - let app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_account_roster(vec![fixture_account_summary()]), - )); - - assert_eq!(app.account_roster, vec![fixture_account_summary()]); - } - - #[test] - fn startup_restores_pending_remote_signer_connection() { - let app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_remote_signer_action(SetupActionState { - label: "Connect Remote Signer".into(), - enabled: true, - pending: false, - }) - .with_pending_remote_signer(Some(fixture_pending_remote_signer())), - )); - - assert_eq!( - app.remote_signer_entry_state, - RemoteSignerEntryState::WaitingApproval(fixture_pending_remote_signer()) - ); - } - - #[test] - fn startup_unsupported_shows_reason() { - let app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(IdentityGateState::Unsupported { - reason: "unsupported".into(), - }), - vec![], - vec![], - SetupActionState { - label: "Connect Browser Signer".into(), - enabled: false, - pending: false, - }, - ))); - assert_eq!(app.screen, AppScreen::Setup); - assert_eq!(app.status_message.as_deref(), Some("unsupported")); - } - - #[test] - fn deferred_setup_action_transitions_to_home_after_poll() { - let mut app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(IdentityGateState::Missing), - vec![Ok(None)], - vec![Ok(Some(fixture_ready_state()))], - SetupActionState { - label: "Connect Browser Signer".into(), - enabled: true, - pending: false, - }, - ))); - - app.request_setup_action(); - assert_eq!(app.screen, AppScreen::Setup); - - app.sync_backend(); - - assert_eq!(app.screen, fixture_home_screen()); - } - - #[test] - fn immediate_setup_action_transitions_to_home() { - let mut app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(IdentityGateState::Missing), - vec![Ok(Some(fixture_ready_state()))], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ))); - - app.request_setup_action(); - - assert_eq!(app.screen, fixture_home_screen()); - } - - #[test] - fn home_setup_action_transitions_to_new_selected_account() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_account_roster(vec![ - fixture_account_summary(), - fixture_bob_account_summary(), - ]) - .with_home_setup_action( - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - vec![Ok(Some(IdentityGateState::Ready { - account_id: FIXTURE_BOB.account_id.into(), - }))], - ), - )); - - app.request_home_setup_action(); - - assert_eq!( - app.screen, - AppScreen::Home { - account_id: FIXTURE_BOB.account_id.into(), - } - ); - assert_eq!(app.account_roster.len(), 2); - } - - #[test] - fn select_account_transitions_to_requested_account() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_account_roster(vec![ - fixture_account_summary(), - fixture_bob_account_summary(), - ]) - .with_select_account( - FIXTURE_BOB.account_id, - vec![Ok(Some(IdentityGateState::Ready { - account_id: FIXTURE_BOB.account_id.into(), - }))], - ), - )); - - app.request_select_account(FIXTURE_BOB.account_id); - - assert_eq!( - app.screen, - AppScreen::Home { - account_id: FIXTURE_BOB.account_id.into(), - } - ); - } - - #[test] - fn home_remove_action_transitions_to_setup() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::RemoveLocalKey, - label: "Remove Key From This Device".into(), - enabled: true, - pending: false, - }, - vec![Ok(HomeActionResult::IdentityState( - IdentityGateState::Missing, - ))], - ), - )); - - app.pending_home_confirmation = Some(HomeActionKind::RemoveLocalKey); - app.request_home_action(HomeActionKind::RemoveLocalKey); - - assert_eq!(app.screen, AppScreen::Setup); - assert_eq!(app.status_message, None); - assert_eq!(app.pending_home_confirmation, None); - } - - #[test] - fn failed_home_remove_action_keeps_home_screen_and_message() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::RemoveLocalKey, - label: "Remove Key From This Device".into(), - enabled: true, - pending: false, - }, - vec![Err("remove failed".into())], - ), - )); - - app.pending_home_confirmation = Some(HomeActionKind::RemoveLocalKey); - app.request_home_action(HomeActionKind::RemoveLocalKey); - - assert!(matches!(app.screen, AppScreen::Home { .. })); - assert_eq!(app.status_message.as_deref(), Some("remove failed")); - assert_eq!( - app.pending_home_confirmation, - Some(HomeActionKind::RemoveLocalKey) - ); - } - - #[test] - fn home_reset_action_transitions_to_setup() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::ResetDevice, - label: "Reset This Device".into(), - enabled: true, - pending: false, - }, - vec![Ok(HomeActionResult::IdentityState( - IdentityGateState::Missing, - ))], - ), - )); - - app.pending_home_confirmation = Some(HomeActionKind::ResetDevice); - app.request_home_action(HomeActionKind::ResetDevice); - - assert_eq!(app.screen, AppScreen::Setup); - assert_eq!(app.status_message, None); - assert_eq!(app.pending_home_confirmation, None); - } - - #[test] - fn failed_home_reset_action_keeps_home_screen_and_message() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::ResetDevice, - label: "Reset This Device".into(), - enabled: true, - pending: false, - }, - vec![Err("reset failed".into())], - ), - )); - - app.pending_home_confirmation = Some(HomeActionKind::ResetDevice); - app.request_home_action(HomeActionKind::ResetDevice); - - assert!(matches!(app.screen, AppScreen::Home { .. })); - assert_eq!(app.status_message.as_deref(), Some("reset failed")); - assert_eq!( - app.pending_home_confirmation, - Some(HomeActionKind::ResetDevice) - ); - } - - #[test] - fn import_action_transitions_to_home() { - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_import_action( - ImportActionState { - label: "Import Secret Key".into(), - enabled: true, - pending: false, - }, - vec![Ok(Some(fixture_ready_state()))], - ), - )); - - app.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey); - app.secret_key_input = Zeroizing::new(encrypted_secret_key); - app.import_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into()); - app.request_import_action(); - - assert_eq!(app.screen, fixture_home_screen()); - assert_eq!(app.pending_import_mode, None); - assert_eq!(app.secret_key_input.as_str(), ""); - assert_eq!(app.import_password_input.as_str(), ""); - } - - #[test] - fn import_paste_action_populates_secret_key_input() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_import_action( - ImportActionState { - label: "Import Secret Key".into(), - enabled: true, - pending: false, - }, - vec![], - ) - .with_import_paste_action( - PasteActionState { - label: "Paste Secret Key".into(), - enabled: true, - pending: false, - }, - vec![Ok(Some(FIXTURE_ALICE.nsec.into()))], - ), - )); - - app.pending_import_mode = Some(RadrootsSecretImportMode::EncryptedSecretKey); - app.request_import_paste_action(); - - assert_eq!(app.secret_key_input.as_str(), FIXTURE_ALICE.nsec); - assert_eq!(app.status_message, None); - } - - #[test] - fn remote_signer_preview_moves_entry_into_review_state() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_remote_signer_action(SetupActionState { - label: "Connect Remote Signer".into(), - enabled: true, - pending: false, - }) - .with_remote_signer_preview(vec![Ok(fixture_remote_signer_preview())]), - )); - - app.open_remote_signer_entry(); - app.remote_signer_input = - Zeroizing::new("http://localhost/connect?uri=bunker%3A%2F%2Fexample".into()); - app.request_remote_signer_preview(); - - assert_eq!( - app.remote_signer_entry_state, - RemoteSignerEntryState::Review(fixture_remote_signer_preview()) - ); - } - - #[test] - fn remote_signer_connect_enters_waiting_state() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_remote_signer_action(SetupActionState { - label: "Connect Remote Signer".into(), - enabled: true, - pending: false, - }) - .with_remote_signer_request(vec![Ok(None)]) - .with_pending_remote_signer(Some(fixture_pending_remote_signer())), - )); - - app.remote_signer_entry_state = - RemoteSignerEntryState::Review(fixture_remote_signer_preview()); - app.remote_signer_input = - Zeroizing::new("http://localhost/connect?uri=bunker%3A%2F%2Fexample".into()); - app.request_remote_signer_connect(); - - assert_eq!( - app.remote_signer_entry_state, - RemoteSignerEntryState::WaitingApproval(fixture_pending_remote_signer()) - ); - } - - #[test] - fn cancel_pending_remote_signer_clears_waiting_state() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_remote_signer_action(SetupActionState { - label: "Connect Remote Signer".into(), - enabled: true, - pending: false, - }) - .with_pending_remote_signer(Some(fixture_pending_remote_signer())) - .with_cancel_pending_remote_signer(vec![Ok(())]), - )); - - app.request_cancel_pending_remote_signer(); - - assert_eq!( - app.remote_signer_entry_state, - RemoteSignerEntryState::Closed - ); - } - - #[test] - fn encrypted_backup_home_action_reveals_secret_key_without_leaving_home() { - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::BackupSecretKey, - label: "Back Up Secret Key".into(), - enabled: true, - pending: false, - }, - vec![], - ) - .with_secret_key_backup_request(vec![Ok( - HomeActionResult::RevealEncryptedSecretKey { - ncryptsec: encrypted_secret_key.clone(), - }, - )]), - )); - - app.pending_secret_key_backup_entry = true; - app.secret_key_backup_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into()); - app.secret_key_backup_password_confirm_input = - Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into()); - app.request_secret_key_backup_action(); - - assert!(matches!(app.screen, AppScreen::Home { .. })); - assert_eq!(app.pending_home_confirmation, None); - assert_eq!(app.pending_secret_key_backup_entry, false); - let Some(RevealedSecretMaterial::EncryptedSecretKey(value)) = - app.revealed_secret_material.as_ref() - else { - panic!("expected encrypted secret backup"); - }; - assert_eq!(value.as_str(), encrypted_secret_key); - } - - #[test] - fn deferred_encrypted_backup_home_action_reveals_secret_key_after_poll() { - let encrypted_secret_key = - fixture_identity_ncryptsec(&FIXTURE_ALICE, FIXTURE_BACKUP_PASSWORD) - .expect("fixture encrypted secret key"); - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::BackupSecretKey, - label: "Back Up Secret Key".into(), - enabled: true, - pending: true, - }, - vec![], - ) - .with_secret_key_backup_request(vec![Ok(HomeActionResult::None)]) - .with_home_action_poll(vec![Ok(Some( - HomeActionResult::RevealEncryptedSecretKey { - ncryptsec: encrypted_secret_key.clone(), - }, - ))]), - )); - - app.pending_secret_key_backup_entry = true; - app.secret_key_backup_password_input = Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into()); - app.secret_key_backup_password_confirm_input = - Zeroizing::new(FIXTURE_BACKUP_PASSWORD.into()); - app.request_secret_key_backup_action(); - assert_eq!(app.revealed_secret_material, None); - - app.sync_backend(); - - let Some(RevealedSecretMaterial::EncryptedSecretKey(value)) = - app.revealed_secret_material.as_ref() - else { - panic!("expected encrypted secret backup"); - }; - assert_eq!(value.as_str(), encrypted_secret_key); - } - - #[test] - fn raw_secret_reveal_home_action_uses_advanced_path() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_home_action( - HomeActionState { - kind: HomeActionKind::RevealRawSecretKey, - label: "Reveal Raw Secret Key".into(), - enabled: true, - pending: false, - }, - vec![Ok(HomeActionResult::RevealRawSecretKey { - nsec: FIXTURE_ALICE.nsec.into(), - })], - ), - )); - - app.pending_home_confirmation = Some(HomeActionKind::RevealRawSecretKey); - app.request_home_action(HomeActionKind::RevealRawSecretKey); - - let Some(RevealedSecretMaterial::RawSecretKey { nsec, .. }) = - app.revealed_secret_material.as_ref() - else { - panic!("expected raw secret reveal"); - }; - assert_eq!(nsec.as_str(), FIXTURE_ALICE.nsec); - } - - #[test] - fn raw_secret_reveal_expires_after_timeout() { - let mut app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ))); - app.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey { - nsec: Zeroizing::new(FIXTURE_ALICE.nsec.into()), - revealed_at: Instant::now() - RAW_SECRET_REVEAL_TIMEOUT - Duration::from_secs(1), - }); - - app.sync_revealed_secret_material_lifetime(); - - assert_eq!(app.revealed_secret_material, None); - } - - #[test] - fn raw_secret_reveal_clears_when_app_loses_focus() { - let mut app = RadrootsApp::new(Box::new(MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ))); - app.revealed_secret_material = Some(RevealedSecretMaterial::RawSecretKey { - nsec: Zeroizing::new(FIXTURE_ALICE.nsec.into()), - revealed_at: Instant::now(), - }); - - let ctx = egui::Context::default(); - ctx.input_mut(|input| { - input - .raw - .viewports - .entry(egui::ViewportId::ROOT) - .or_default() - .focused = Some(false); - }); - app.clear_raw_secret_when_app_unfocused(&ctx); - - assert_eq!(app.revealed_secret_material, None); - } - - #[test] - fn deferred_home_location_lookup_updates_after_poll() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(fixture_ready_state()), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_offline_geocoder_state(RadrootsOfflineGeocoderState::Ready, vec![]) - .with_reverse_lookup( - vec![Ok(())], - vec![Ok(Some(Ok(vec![RadrootsResolvedLocation { - id: 7, - name: "Paris".into(), - admin1_id: Some(11), - admin1_name: Some("Ile-de-France".into()), - country_id: "FR".into(), - country_name: Some("France".into()), - point: RadrootsLocationPoint { - lat: 48.8566, - lng: 2.3522, - }, - }])))], - ), - )); - - app.home_location_tools - .set_query_inputs("48.8566", "2.3522"); - app.home_location_tools - .begin_resolve_with_backend(app.backend.as_ref()); - assert!(app.home_location_tools.is_pending()); - - app.sync_backend(); - - assert_eq!(app.home_location_tools.status_message(), None); - assert_eq!( - app.home_location_tools - .lookup_result() - .as_ref() - .map(|result| result.matches[0].name.as_str()), - Some("Paris") - ); - } - - #[test] - fn startup_uses_initial_offline_geocoder_state() { - let app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_offline_geocoder_state(RadrootsOfflineGeocoderState::Initializing, vec![]), - )); - - assert_eq!( - app.offline_geocoder_state, - Some(RadrootsOfflineGeocoderState::Initializing) - ); - } - - #[test] - fn offline_geocoder_state_updates_after_poll() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_offline_geocoder_state( - RadrootsOfflineGeocoderState::Initializing, - vec![Ok(Some(RadrootsOfflineGeocoderState::Ready))], - ), - )); - - app.sync_backend(); - - assert_eq!( - app.offline_geocoder_state, - Some(RadrootsOfflineGeocoderState::Ready) - ); - } - - #[test] - fn offline_geocoder_failure_keeps_user_and_debug_messages() { - let mut app = RadrootsApp::new(Box::new( - MockBackend::new( - Ok(IdentityGateState::Missing), - vec![], - vec![], - SetupActionState { - label: "Generate New Key".into(), - enabled: true, - pending: false, - }, - ) - .with_offline_geocoder_state( - RadrootsOfflineGeocoderState::Initializing, - vec![Ok(Some(RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Desktop, - "failed to open staged geocoder db", - )))], - ), - )); - - app.sync_backend(); - - assert_eq!( - app.offline_geocoder_state, - Some(RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Desktop, - "failed to open staged geocoder db", - )) - ); - assert_eq!( - app.offline_geocoder_state - .as_ref() - .and_then(RadrootsOfflineGeocoderState::user_message), - Some("Offline geocoder could not be initialized on this device.") - ); - assert_eq!( - app.offline_geocoder_state - .as_ref() - .and_then(RadrootsOfflineGeocoderState::debug_message), - Some("failed to open staged geocoder db") - ); - let diagnostic = app - .offline_geocoder_state - .as_ref() - .and_then(RadrootsOfflineGeocoderState::diagnostic) - .unwrap(); - assert_eq!(diagnostic.platform_code, "desktop"); - assert_eq!(diagnostic.asset_revision, None); - assert_eq!(diagnostic.code, "initialization_failed"); - assert!( - !diagnostic - .export_text() - .contains("failed to open staged geocoder db") - ); - } -} diff --git a/crates/shared/core/src/location_resolver.rs b/crates/shared/core/src/location_resolver.rs @@ -1,123 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct RadrootsLocationPoint { - pub lat: f64, - pub lng: f64, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct RadrootsLocationReverseOptions { - pub limit: usize, - pub degree_offset: f64, -} - -impl Default for RadrootsLocationReverseOptions { - fn default() -> Self { - Self { - limit: 1, - degree_offset: 0.5, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RadrootsResolvedLocation { - pub id: i64, - pub name: String, - pub admin1_id: Option<i64>, - pub admin1_name: Option<String>, - pub country_id: String, - pub country_name: Option<String>, - pub point: RadrootsLocationPoint, -} - -pub type RadrootsReverseLocationLookupResult = - Result<Vec<RadrootsResolvedLocation>, RadrootsLocationResolverError>; - -#[derive(Debug, Clone, PartialEq)] -pub struct RadrootsLocationCountry { - pub country_id: String, - pub country_name: Option<String>, - pub center: RadrootsLocationPoint, -} - -pub type RadrootsLocationCountryListResult = - Result<Vec<RadrootsLocationCountry>, RadrootsLocationResolverError>; - -pub type RadrootsLocationCountryCenterLookupResult = - Result<RadrootsLocationPoint, RadrootsLocationResolverError>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsLocationResolverError { - Unsupported, - Initializing, - Unavailable, - CountryCenterNotFound { country_id: String }, - QueryFailed { message: String }, -} - -impl RadrootsLocationResolverError { - pub fn code(&self) -> &'static str { - match self { - Self::Unsupported => "unsupported", - Self::Initializing => "initializing", - Self::Unavailable => "unavailable", - Self::CountryCenterNotFound { .. } => "country_center_not_found", - Self::QueryFailed { .. } => "query_failed", - } - } - - pub fn user_message(&self) -> &'static str { - match self { - Self::Unsupported => "Offline location resolution is not available on this platform.", - Self::Initializing => { - "Offline location resolution is still initializing on this device." - } - Self::Unavailable => "Offline location resolution is not available on this device.", - Self::CountryCenterNotFound { .. } => "The requested country center is not available.", - Self::QueryFailed { .. } => "The offline location query could not be completed.", - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn reverse_options_default_matches_geocoder_defaults() { - let options = RadrootsLocationReverseOptions::default(); - - assert_eq!(options.limit, 1); - assert_eq!(options.degree_offset, 0.5); - } - - #[test] - fn location_resolver_error_codes_are_stable() { - assert_eq!( - RadrootsLocationResolverError::Unsupported.code(), - "unsupported" - ); - assert_eq!( - RadrootsLocationResolverError::Initializing.code(), - "initializing" - ); - assert_eq!( - RadrootsLocationResolverError::Unavailable.code(), - "unavailable" - ); - assert_eq!( - RadrootsLocationResolverError::CountryCenterNotFound { - country_id: "US".to_owned(), - } - .code(), - "country_center_not_found" - ); - assert_eq!( - RadrootsLocationResolverError::QueryFailed { - message: "sqlite failed".to_owned(), - } - .code(), - "query_failed" - ); - } -} diff --git a/crates/shared/core/src/offline_geocoder.rs b/crates/shared/core/src/offline_geocoder.rs @@ -1,237 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RadrootsOfflineGeocoderPlatform { - Desktop, - Ios, - Android, - Web, -} - -impl RadrootsOfflineGeocoderPlatform { - pub fn code(self) -> &'static str { - match self { - Self::Desktop => "desktop", - Self::Ios => "ios", - Self::Android => "android", - Self::Web => "web", - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RadrootsOfflineGeocoderUnavailableKind { - MissingBuildAsset, - InitializationFailed, - InternalError, -} - -impl RadrootsOfflineGeocoderUnavailableKind { - pub fn code(self) -> &'static str { - match self { - Self::MissingBuildAsset => "missing_build_asset", - Self::InitializationFailed => "initialization_failed", - Self::InternalError => "internal_error", - } - } - - pub fn technical_message(self) -> &'static str { - match self { - Self::MissingBuildAsset => { - "The offline geocoder data file is missing from this app build." - } - Self::InitializationFailed => { - "The offline geocoder data file could not be prepared on this device." - } - Self::InternalError => { - "The app could not complete offline geocoder setup because of an internal error." - } - } - } - - pub fn user_message(self) -> &'static str { - match self { - Self::MissingBuildAsset => "Offline geocoder is not available in this build.", - Self::InitializationFailed | Self::InternalError => { - "Offline geocoder could not be initialized on this device." - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsOfflineGeocoderDiagnostic { - pub platform_code: &'static str, - pub asset_revision: Option<String>, - pub code: &'static str, - pub summary_label: &'static str, - pub user_message: &'static str, - pub technical_message: &'static str, -} - -impl RadrootsOfflineGeocoderDiagnostic { - pub fn export_text(&self) -> String { - format!( - "offline geocoder diagnostic\nplatform: {}\nasset_revision: {}\ncode: {}\nstatus: {}\nuser: {}\ntechnical: {}", - self.platform_code, - self.asset_revision.as_deref().unwrap_or("unknown"), - self.code, - self.summary_label, - self.user_message, - self.technical_message - ) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsOfflineGeocoderState { - Initializing, - Ready, - Unavailable { - kind: RadrootsOfflineGeocoderUnavailableKind, - platform: RadrootsOfflineGeocoderPlatform, - asset_revision: Option<String>, - debug_message: String, - }, -} - -impl RadrootsOfflineGeocoderState { - pub fn unavailable( - kind: RadrootsOfflineGeocoderUnavailableKind, - platform: RadrootsOfflineGeocoderPlatform, - debug_message: impl Into<String>, - ) -> Self { - Self::Unavailable { - kind, - platform, - asset_revision: None, - debug_message: debug_message.into(), - } - } - - pub fn unavailable_with_revision( - kind: RadrootsOfflineGeocoderUnavailableKind, - platform: RadrootsOfflineGeocoderPlatform, - asset_revision: impl Into<String>, - debug_message: impl Into<String>, - ) -> Self { - Self::Unavailable { - kind, - platform, - asset_revision: Some(asset_revision.into()), - debug_message: debug_message.into(), - } - } - - pub fn debug_message(&self) -> Option<&str> { - match self { - Self::Unavailable { debug_message, .. } => Some(debug_message.as_str()), - Self::Initializing | Self::Ready => None, - } - } - - pub fn diagnostic(&self) -> Option<RadrootsOfflineGeocoderDiagnostic> { - match self { - Self::Unavailable { - kind, - platform, - asset_revision, - .. - } => Some(RadrootsOfflineGeocoderDiagnostic { - platform_code: platform.code(), - asset_revision: asset_revision.clone(), - code: kind.code(), - summary_label: self.summary_label(), - user_message: kind.user_message(), - technical_message: kind.technical_message(), - }), - Self::Initializing | Self::Ready => None, - } - } - - pub fn summary_label(&self) -> &'static str { - match self { - Self::Initializing => "Offline geocoder: initializing", - Self::Ready => "Offline geocoder: ready", - Self::Unavailable { .. } => "Offline geocoder unavailable", - } - } - - pub fn platform(&self) -> Option<RadrootsOfflineGeocoderPlatform> { - match self { - Self::Unavailable { platform, .. } => Some(*platform), - Self::Initializing | Self::Ready => None, - } - } - - pub fn asset_revision(&self) -> Option<&str> { - match self { - Self::Unavailable { asset_revision, .. } => asset_revision.as_deref(), - Self::Initializing | Self::Ready => None, - } - } - - pub fn technical_message(&self) -> Option<&'static str> { - match self { - Self::Unavailable { kind, .. } => Some(kind.technical_message()), - Self::Initializing | Self::Ready => None, - } - } - - pub fn user_message(&self) -> Option<&'static str> { - match self { - Self::Unavailable { kind, .. } => Some(kind.user_message()), - Self::Initializing | Self::Ready => None, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn unavailable_state_exposes_release_safe_diagnostic() { - let state = RadrootsOfflineGeocoderState::unavailable( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Desktop, - "failed to open staged geocoder db: /tmp/geonames.db", - ); - let diagnostic = state.diagnostic().unwrap(); - - assert_eq!(diagnostic.platform_code, "desktop"); - assert_eq!(diagnostic.asset_revision, None); - assert_eq!(diagnostic.code, "initialization_failed"); - assert_eq!(diagnostic.summary_label, "Offline geocoder unavailable"); - assert_eq!( - diagnostic.user_message, - "Offline geocoder could not be initialized on this device." - ); - assert_eq!( - diagnostic.technical_message, - "The offline geocoder data file could not be prepared on this device." - ); - assert!(!diagnostic.export_text().contains("/tmp/geonames.db")); - } - - #[test] - fn unavailable_state_with_revision_exports_release_safe_platform_context() { - let state = RadrootsOfflineGeocoderState::unavailable_with_revision( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - RadrootsOfflineGeocoderPlatform::Android, - "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c", - "failed to open staged android geocoder db: /data/user/0/org.radroots.app.android/files/geocoder.db", - ); - let diagnostic = state.diagnostic().unwrap(); - let export_text = diagnostic.export_text(); - - assert_eq!(diagnostic.platform_code, "android"); - assert_eq!( - diagnostic.asset_revision.as_deref(), - Some("6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c") - ); - assert!(export_text.contains("platform: android")); - assert!(export_text.contains( - "asset_revision: 6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" - )); - assert!(!export_text.contains("/data/user/0/org.radroots.app.android/files/geocoder.db")); - } -} diff --git a/crates/shared/core/src/remote_signer.rs b/crates/shared/core/src/remote_signer.rs @@ -1,29 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsRemoteSignerPreview { - pub source_label: String, - pub signer_npub: String, - pub relays: Vec<String>, - pub requested_permissions: Vec<String>, -} - -impl RadrootsRemoteSignerPreview { - pub fn pending_summary(&self) -> RadrootsPendingRemoteSignerConnection { - RadrootsPendingRemoteSignerConnection { - signer_npub: self.signer_npub.clone(), - relays: self.relays.clone(), - auth_url: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsPendingRemoteSignerConnection { - pub signer_npub: String, - pub relays: Vec<String>, - pub auth_url: Option<String>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsRemoteSignerSignedNote { - pub event_id_hex: String, -} diff --git a/crates/shared/core/src/secret_keys.rs b/crates/shared/core/src/secret_keys.rs @@ -1,58 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum RadrootsSecretImportMode { - #[default] - EncryptedSecretKey, - RawSecretKey, -} - -impl RadrootsSecretImportMode { - pub fn helper_text(self) -> &'static str { - match self { - Self::EncryptedSecretKey => { - "Import an existing local identity by entering its encrypted secret key and password." - } - Self::RawSecretKey => { - "Advanced: import an existing local identity by entering its raw nsec secret key." - } - } - } - - pub fn hint_text(self) -> &'static str { - match self { - Self::EncryptedSecretKey => "ncryptsec1...", - Self::RawSecretKey => "nsec1...", - } - } - - pub fn mode_label(self) -> &'static str { - match self { - Self::EncryptedSecretKey => "Encrypted Secret Key", - Self::RawSecretKey => "Raw Secret Key", - } - } - - pub fn switch_label(self) -> &'static str { - match self { - Self::EncryptedSecretKey => "Use Raw Secret Key Instead", - Self::RawSecretKey => "Use Encrypted Secret Key Instead", - } - } - - pub fn requires_password(self) -> bool { - matches!(self, Self::EncryptedSecretKey) - } - - pub fn toggle(self) -> Self { - match self { - Self::EncryptedSecretKey => Self::RawSecretKey, - Self::RawSecretKey => Self::EncryptedSecretKey, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsSecretImportRequest { - pub mode: RadrootsSecretImportMode, - pub secret_text: String, - pub password: Option<String>, -} diff --git a/crates/shared/core/src/storage_paths.rs b/crates/shared/core/src/storage_paths.rs @@ -1,122 +0,0 @@ -use std::path::{Path, PathBuf}; - -use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, - RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace, -}; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsAppStorageLayout { - pub runtime_root: PathBuf, - pub app_paths: RadrootsPaths, -} - -fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> { - RadrootsRuntimeNamespace::app("app") - .map_err(|source| format!("failed to resolve app runtime namespace: {source}")) -} - -fn runtime_root_from_paths(roots: &RadrootsPaths) -> Result<PathBuf, String> { - roots - .config - .parent() - .map(Path::to_path_buf) - .ok_or_else(|| "resolved app config root had no parent".to_owned()) -} - -pub fn interactive_user_app_storage_layout_with_resolver( - resolver: &RadrootsPathResolver, -) -> Result<RadrootsAppStorageLayout, String> { - let roots = resolver - .resolve( - RadrootsPathProfile::InteractiveUser, - &RadrootsPathOverrides::default(), - ) - .map_err(|source| format!("failed to resolve app interactive-user roots: {source}"))?; - let namespace = app_namespace()?; - Ok(RadrootsAppStorageLayout { - runtime_root: runtime_root_from_paths(&roots)?, - app_paths: roots.namespaced(&namespace), - }) -} - -pub fn mobile_native_app_storage_layout( - platform: RadrootsPlatform, - base_root: &Path, -) -> Result<RadrootsAppStorageLayout, String> { - let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); - let roots = resolver - .resolve( - RadrootsPathProfile::MobileNative, - &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)), - ) - .map_err(|source| format!("failed to resolve app mobile-native roots: {source}"))?; - let namespace = app_namespace()?; - Ok(RadrootsAppStorageLayout { - runtime_root: runtime_root_from_paths(&roots)?, - app_paths: roots.namespaced(&namespace), - }) -} - -#[cfg(test)] -mod tests { - use super::{ - interactive_user_app_storage_layout_with_resolver, mobile_native_app_storage_layout, - }; - use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform}; - use std::path::PathBuf; - - #[test] - fn interactive_user_layout_keeps_runtime_root_and_namespaced_paths() { - let resolver = RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment { - home_dir: Some(PathBuf::from("/home/treesap")), - ..RadrootsHostEnvironment::default() - }, - ); - - let layout = - interactive_user_app_storage_layout_with_resolver(&resolver).expect("app layout"); - - assert_eq!( - layout.runtime_root, - PathBuf::from("/home/treesap/.radroots") - ); - assert_eq!( - layout.app_paths.data, - PathBuf::from("/home/treesap/.radroots/data/apps/app") - ); - assert_eq!( - layout.app_paths.logs, - PathBuf::from("/home/treesap/.radroots/logs/apps/app") - ); - } - - #[test] - fn mobile_native_layout_keeps_explicit_runtime_root_and_namespaced_paths() { - let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots"); - - let layout = - mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root.as_path()) - .expect("mobile layout"); - - assert_eq!(layout.runtime_root, base_root); - assert_eq!( - layout.app_paths.config, - PathBuf::from( - "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app" - ) - ); - assert_eq!( - layout.app_paths.data, - PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app") - ); - assert_eq!( - layout.app_paths.secrets, - PathBuf::from( - "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app" - ) - ); - } -} diff --git a/crates/shared/remote_signer/Cargo.toml b/crates/shared/remote_signer/Cargo.toml @@ -1,29 +0,0 @@ -[package] -name = "radroots_app_remote_signer" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots app local remote signer session support" -publish = false - -[lints] -workspace = true - -[dependencies] -nostr = { workspace = true, features = ["nip44"] } -radroots_identity.workspace = true -radroots_nostr_accounts = { workspace = true, features = ["memory-vault"] } -radroots_nostr.workspace = true -radroots_nostr_connect.workspace = true -serde.workspace = true -serde_json.workspace = true -tokio.workspace = true -url.workspace = true - -[dev-dependencies] -radroots_app_test_support = { path = "../test_support" } -tempfile = "3.23.0" diff --git a/crates/shared/remote_signer/src/action.rs b/crates/shared/remote_signer/src/action.rs @@ -1,327 +0,0 @@ -use crate::protocol::{ - RadrootsAppRemoteSignerProgressUpdate, RadrootsAppRemoteSignerSignedEvent, - radroots_app_remote_signer_sign_kind1_note_with_progress, -}; -use crate::session::RadrootsAppRemoteSignerSessionRecord; -use std::marker::PhantomData; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; - -type RadrootsAppRemoteSignerSignNoteFn = Arc< - dyn Fn( - &RadrootsAppRemoteSignerSessionRecord, - &str, - &str, - Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>, - ) -> Result<RadrootsAppRemoteSignerSignedEvent, String> - + Send - + Sync, ->; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsAppRemoteSignerActionState { - Idle, - Signing, - AwaitingAuthorization { url: String }, -} - -pub trait RadrootsAppRemoteSignerActionControllerHooks: Clone + Send + Sync + 'static { - type ReadyState: Send + Sync + 'static; - - fn selected_active_session( - &self, - ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String>; - - fn complete_sign_event( - &self, - signed_event: RadrootsAppRemoteSignerSignedEvent, - ) -> Result<Self::ReadyState, String>; -} - -pub struct RadrootsAppRemoteSignerActionController<H> -where - H: RadrootsAppRemoteSignerActionControllerHooks, -{ - hooks: H, - sign_kind1_note: RadrootsAppRemoteSignerSignNoteFn, - update: Arc<Mutex<Option<Result<Option<H::ReadyState>, String>>>>, - changed: Arc<AtomicBool>, - signing: Arc<AtomicBool>, - state: Arc<Mutex<RadrootsAppRemoteSignerActionState>>, - _ready_state: PhantomData<H::ReadyState>, -} - -impl<H> Clone for RadrootsAppRemoteSignerActionController<H> -where - H: RadrootsAppRemoteSignerActionControllerHooks, -{ - fn clone(&self) -> Self { - Self { - hooks: self.hooks.clone(), - sign_kind1_note: Arc::clone(&self.sign_kind1_note), - update: Arc::clone(&self.update), - changed: Arc::clone(&self.changed), - signing: Arc::clone(&self.signing), - state: Arc::clone(&self.state), - _ready_state: PhantomData, - } - } -} - -impl<H> RadrootsAppRemoteSignerActionController<H> -where - H: RadrootsAppRemoteSignerActionControllerHooks, -{ - pub fn new(hooks: H) -> Self { - Self { - hooks, - sign_kind1_note: Arc::new(default_sign_kind1_note), - update: Arc::new(Mutex::new(None)), - changed: Arc::new(AtomicBool::new(false)), - signing: Arc::new(AtomicBool::new(false)), - state: Arc::new(Mutex::new(RadrootsAppRemoteSignerActionState::Idle)), - _ready_state: PhantomData, - } - } - - #[cfg(test)] - fn new_with_ops(hooks: H, sign_kind1_note: RadrootsAppRemoteSignerSignNoteFn) -> Self { - Self { - hooks, - sign_kind1_note, - update: Arc::new(Mutex::new(None)), - changed: Arc::new(AtomicBool::new(false)), - signing: Arc::new(AtomicBool::new(false)), - state: Arc::new(Mutex::new(RadrootsAppRemoteSignerActionState::Idle)), - _ready_state: PhantomData, - } - } - - pub fn take_update(&self) -> Option<Result<Option<H::ReadyState>, String>> { - if !self.changed.swap(false, Ordering::AcqRel) { - return None; - } - self.update.lock().ok().and_then(|mut slot| slot.take()) - } - - pub fn is_signing(&self) -> bool { - self.signing.load(Ordering::Acquire) - } - - pub fn state(&self) -> RadrootsAppRemoteSignerActionState { - self.state - .lock() - .map(|state| state.clone()) - .unwrap_or(RadrootsAppRemoteSignerActionState::Idle) - } - - pub fn begin_sign_kind1_note(&self, content: &str) -> Result<(), String> { - if self.signing.swap(true, Ordering::AcqRel) { - return Err("remote signer note signing is already running".to_owned()); - } - - let Some((record, client_secret_key_hex)) = self.hooks.selected_active_session()? else { - self.signing.store(false, Ordering::Release); - return Err("select a remote signer account before signing a note".to_owned()); - }; - let note_content = content.trim().to_owned(); - if note_content.is_empty() { - self.signing.store(false, Ordering::Release); - return Err("enter a note before requesting a remote signature".to_owned()); - } - - self.set_state(RadrootsAppRemoteSignerActionState::Signing); - if let Ok(mut slot) = self.update.lock() { - *slot = None; - } - - let tracker = self.clone(); - std::thread::spawn(move || { - let progress_tracker = tracker.clone(); - let progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync> = - Arc::new(move |update| progress_tracker.apply_progress(update)); - let outcome = (tracker.sign_kind1_note)( - &record, - client_secret_key_hex.as_str(), - note_content.as_str(), - progress, - ) - .and_then(|signed_event| tracker.hooks.complete_sign_event(signed_event)); - - tracker.set_state(RadrootsAppRemoteSignerActionState::Idle); - tracker.signing.store(false, Ordering::Release); - match outcome { - Ok(result) => tracker.push_update(Ok(Some(result))), - Err(error) => tracker.push_update(Err(error)), - } - }); - - Ok(()) - } - - fn apply_progress(&self, update: RadrootsAppRemoteSignerProgressUpdate) { - match update { - RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url } => { - let next = - RadrootsAppRemoteSignerActionState::AwaitingAuthorization { url: url.clone() }; - if self.set_state(next) { - self.push_update(Err(format!( - "authorize the remote signer to continue: {url}" - ))); - } - } - } - } - - fn push_update(&self, result: Result<Option<H::ReadyState>, String>) { - if let Ok(mut slot) = self.update.lock() { - *slot = Some(result); - self.changed.store(true, Ordering::Release); - } - } - - fn set_state(&self, next: RadrootsAppRemoteSignerActionState) -> bool { - if let Ok(mut state) = self.state.lock() { - if *state == next { - return false; - } - *state = next; - return true; - } - false - } -} - -fn default_sign_kind1_note( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - content: &str, - progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>, -) -> Result<RadrootsAppRemoteSignerSignedEvent, String> { - radroots_app_remote_signer_sign_kind1_note_with_progress( - record, - client_secret_key_hex, - content, - move |update| progress(update), - ) - .map_err(|error| error.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::session::RadrootsAppRemoteSignerSessionRecord; - use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, fixture_identity}; - use std::sync::mpsc; - use std::sync::{Condvar, Mutex}; - use std::time::Duration; - - #[derive(Clone)] - struct TestHooks { - session: Option<(RadrootsAppRemoteSignerSessionRecord, String)>, - } - - impl RadrootsAppRemoteSignerActionControllerHooks for TestHooks { - type ReadyState = String; - - fn selected_active_session( - &self, - ) -> Result<Option<(RadrootsAppRemoteSignerSessionRecord, String)>, String> { - Ok(self.session.clone()) - } - - fn complete_sign_event( - &self, - signed_event: RadrootsAppRemoteSignerSignedEvent, - ) -> Result<Self::ReadyState, String> { - Ok(signed_event.event_id_hex) - } - } - - fn fixture_session() -> RadrootsAppRemoteSignerSessionRecord { - let client = fixture_identity(&FIXTURE_ALICE) - .expect("client") - .to_public(); - let signer = fixture_identity(&FIXTURE_BOB).expect("signer").to_public(); - let mut record = RadrootsAppRemoteSignerSessionRecord::pending( - client, - signer.clone(), - vec!["ws://localhost:8080".to_owned()], - ); - record.user_identity = Some(signer); - record.status = crate::session::RadrootsAppRemoteSignerSessionStatus::Active; - record - } - - fn wait_for_update( - controller: &RadrootsAppRemoteSignerActionController<TestHooks>, - ) -> Result<Option<String>, String> { - let deadline = std::time::Instant::now() + Duration::from_secs(2); - loop { - if let Some(update) = controller.take_update() { - return update; - } - if std::time::Instant::now() >= deadline { - panic!("timed out waiting for action update"); - } - std::thread::sleep(Duration::from_millis(10)); - } - } - - #[test] - fn sign_controller_reports_auth_challenge_then_success() { - let hooks = TestHooks { - session: Some((fixture_session(), "client-secret".to_owned())), - }; - let (challenge_seen_tx, challenge_seen_rx) = mpsc::channel(); - let release_gate = Arc::new((Mutex::new(false), Condvar::new())); - let controller = RadrootsAppRemoteSignerActionController::new_with_ops( - hooks, - Arc::new({ - let release_gate = Arc::clone(&release_gate); - move |_, _, _, progress| { - progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { - url: "http://localhost/auth".to_owned(), - }); - challenge_seen_tx.send(()).expect("challenge seen"); - let (released, condvar) = &*release_gate; - let mut released = released.lock().expect("release gate lock"); - while !*released { - released = condvar.wait(released).expect("release gate wait"); - } - Ok(RadrootsAppRemoteSignerSignedEvent { - event_id_hex: "deadbeef".to_owned(), - event_json: "{\"id\":\"deadbeef\"}".to_owned(), - relays: vec!["ws://localhost:8080".to_owned()], - }) - } - }), - ); - - controller - .begin_sign_kind1_note("hello from remote signer") - .expect("begin signing"); - - challenge_seen_rx - .recv_timeout(Duration::from_secs(1)) - .expect("challenge notification"); - let first = wait_for_update(&controller).expect_err("auth challenge status"); - assert_eq!( - first, - "authorize the remote signer to continue: http://localhost/auth" - ); - assert_eq!( - controller.state(), - RadrootsAppRemoteSignerActionState::AwaitingAuthorization { - url: "http://localhost/auth".to_owned() - } - ); - - let (released, condvar) = &*release_gate; - *released.lock().expect("release gate lock") = true; - condvar.notify_one(); - let second = wait_for_update(&controller).expect("signed"); - assert_eq!(second, Some("deadbeef".to_owned())); - assert_eq!(controller.state(), RadrootsAppRemoteSignerActionState::Idle); - } -} diff --git a/crates/shared/remote_signer/src/controller.rs b/crates/shared/remote_signer/src/controller.rs @@ -1,852 +0,0 @@ -use crate::protocol::{ - RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, - RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerPendingSession, - RadrootsAppRemoteSignerProgressUpdate, radroots_app_remote_signer_connect_pending, - radroots_app_remote_signer_open_pending_poller, - radroots_app_remote_signer_poll_pending_poller_with_progress, -}; -use crate::session::RadrootsAppRemoteSignerSessionRecord; -use std::marker::PhantomData; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -type RadrootsAppRemoteSignerConnectPendingFn = - Arc<dyn Fn(&str) -> Result<RadrootsAppRemoteSignerPendingSession, String> + Send + Sync>; -type RadrootsAppRemoteSignerPollPendingFn = Arc< - dyn Fn( - &RadrootsAppRemoteSignerSessionRecord, - &str, - Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync>, - ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, String> - + Send - + Sync, ->; -type RadrootsAppRemoteSignerSleepFn = Arc<dyn Fn(Duration) + Send + Sync>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsAppRemoteSignerPendingState { - Idle, - WaitingApproval, - AwaitingAuthorization { url: String }, - TransportFailure { message: String }, -} - -pub trait RadrootsAppRemoteSignerControllerHooks: Clone + Send + Sync + 'static { - type ReadyState: Send + Sync + 'static; - - fn reconcile_startup_state(&self) -> Result<(), String> { - Ok(()) - } - - fn store_pending_session( - &self, - pending: &RadrootsAppRemoteSignerPendingSession, - ) -> Result<(), String>; - - fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String>; - - fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String>; - - fn activate_pending_session( - &self, - client_account_id: &str, - approved: RadrootsAppRemoteSignerApprovedSession, - ) -> Result<Self::ReadyState, String>; - - fn clear_pending_session(&self) - -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String>; -} - -pub struct RadrootsAppRemoteSignerController<H> -where - H: RadrootsAppRemoteSignerControllerHooks, -{ - hooks: H, - connect_pending: RadrootsAppRemoteSignerConnectPendingFn, - poll_pending: RadrootsAppRemoteSignerPollPendingFn, - sleep: RadrootsAppRemoteSignerSleepFn, - update: Arc<Mutex<Option<Result<Option<H::ReadyState>, String>>>>, - changed: Arc<AtomicBool>, - connecting: Arc<AtomicBool>, - polling: Arc<AtomicBool>, - poll_generation: Arc<AtomicU64>, - pending_state: Arc<Mutex<RadrootsAppRemoteSignerPendingState>>, - _ready_state: PhantomData<H::ReadyState>, -} - -impl<H> Clone for RadrootsAppRemoteSignerController<H> -where - H: RadrootsAppRemoteSignerControllerHooks, -{ - fn clone(&self) -> Self { - Self { - hooks: self.hooks.clone(), - connect_pending: Arc::clone(&self.connect_pending), - poll_pending: Arc::clone(&self.poll_pending), - sleep: Arc::clone(&self.sleep), - update: Arc::clone(&self.update), - changed: Arc::clone(&self.changed), - connecting: Arc::clone(&self.connecting), - polling: Arc::clone(&self.polling), - poll_generation: Arc::clone(&self.poll_generation), - pending_state: Arc::clone(&self.pending_state), - _ready_state: PhantomData, - } - } -} - -impl<H> RadrootsAppRemoteSignerController<H> -where - H: RadrootsAppRemoteSignerControllerHooks, -{ - pub fn new(hooks: H) -> Self { - Self::new_with_ops( - hooks, - Arc::new(default_connect_pending), - default_poll_pending(), - Arc::new(std::thread::sleep), - ) - } - - fn new_with_ops( - hooks: H, - connect_pending: RadrootsAppRemoteSignerConnectPendingFn, - poll_pending: RadrootsAppRemoteSignerPollPendingFn, - sleep: RadrootsAppRemoteSignerSleepFn, - ) -> Self { - let controller = Self { - hooks, - connect_pending, - poll_pending, - sleep, - update: Arc::new(Mutex::new(None)), - changed: Arc::new(AtomicBool::new(false)), - connecting: Arc::new(AtomicBool::new(false)), - polling: Arc::new(AtomicBool::new(false)), - poll_generation: Arc::new(AtomicU64::new(0)), - pending_state: Arc::new(Mutex::new(RadrootsAppRemoteSignerPendingState::Idle)), - _ready_state: PhantomData, - }; - if let Err(error) = controller.reconcile_startup_state() { - controller.push_update(Err(error)); - } else if let Err(error) = controller.resume_pending() { - controller.push_update(Err(error)); - } - controller - } - - pub fn take_update(&self) -> Option<Result<Option<H::ReadyState>, String>> { - if !self.changed.swap(false, Ordering::AcqRel) { - return None; - } - - self.update.lock().ok().and_then(|mut slot| slot.take()) - } - - pub fn is_connecting(&self) -> bool { - self.connecting.load(Ordering::Acquire) - } - - pub fn pending_state(&self) -> RadrootsAppRemoteSignerPendingState { - self.pending_state - .lock() - .map(|state| state.clone()) - .unwrap_or(RadrootsAppRemoteSignerPendingState::Idle) - } - - pub fn begin_connect(&self, input: &str) -> Result<(), String> { - if self.connecting.swap(true, Ordering::AcqRel) { - return Err("remote signer connection is already starting".to_owned()); - } - - if self.pending_session_record()?.is_some() { - self.connecting.store(false, Ordering::Release); - return Err("a remote signer connection is already pending approval".to_owned()); - } - - if let Ok(mut slot) = self.update.lock() { - *slot = None; - } - self.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - - let tracker = self.clone(); - let input = input.to_owned(); - std::thread::spawn(move || { - let outcome = (|| -> Result<(), String> { - let pending = (tracker.connect_pending)(input.as_str())?; - tracker.hooks.store_pending_session(&pending)?; - tracker.start_polling(); - Ok(()) - })(); - - if let Err(error) = outcome { - tracker.push_update(Err(error)); - } - tracker.connecting.store(false, Ordering::Release); - }); - - Ok(()) - } - - pub fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - self.hooks.pending_session_record() - } - - fn reconcile_startup_state(&self) -> Result<(), String> { - self.hooks.reconcile_startup_state() - } - - fn resume_pending(&self) -> Result<(), String> { - let Some(record) = self.pending_session_record()? else { - return Ok(()); - }; - self.hooks - .load_pending_client_secret(record.client_account_id())?; - self.start_polling(); - Ok(()) - } - - fn start_polling(&self) { - let request_generation = self.poll_generation.fetch_add(1, Ordering::AcqRel) + 1; - if self.polling.swap(true, Ordering::AcqRel) { - return; - } - - let tracker = self.clone(); - std::thread::spawn(move || { - loop { - let pending_record = match tracker.hooks.pending_session_record() { - Ok(Some(record)) => record, - Ok(None) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - tracker.finish_polling(request_generation); - return; - } - Err(error) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - tracker.push_update(Err(error)); - tracker.finish_polling(request_generation); - return; - } - }; - let client_secret_key_hex = match tracker - .hooks - .load_pending_client_secret(pending_record.client_account_id()) - { - Ok(secret) => secret, - Err(error) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - tracker.push_update(Err(error)); - tracker.finish_polling(request_generation); - return; - } - }; - - let progress_tracker = tracker.clone(); - let progress: Arc<dyn Fn(RadrootsAppRemoteSignerProgressUpdate) + Send + Sync> = - Arc::new(move |update| progress_tracker.apply_progress(update)); - - match (tracker.poll_pending)( - &pending_record, - client_secret_key_hex.as_str(), - progress, - ) { - Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) => { - tracker.set_pending_state( - RadrootsAppRemoteSignerPendingState::WaitingApproval, - ); - (tracker.sleep)(Duration::from_secs(1)); - } - Ok(RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }) => { - let changed = tracker.set_pending_state( - RadrootsAppRemoteSignerPendingState::TransportFailure { - message: message.clone(), - }, - ); - if changed { - tracker.push_update(Err(format!( - "remote signer approval check failed: {message}" - ))); - } - (tracker.sleep)(Duration::from_secs(1)); - } - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Approved(approved)) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - let ready_state = match tracker - .hooks - .activate_pending_session(pending_record.client_account_id(), approved) - { - Ok(state) => state, - Err(error) => { - tracker - .set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - tracker.push_update(Err(error)); - tracker.finish_polling(request_generation); - return; - } - }; - tracker.push_update(Ok(Some(ready_state))); - tracker.finish_polling(request_generation); - return; - } - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message }) - | Ok(RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message }) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - let outcome = tracker - .hooks - .clear_pending_session() - .map(|_| None) - .unwrap_or_else(|cleanup_error| Some(cleanup_error)); - let error = match outcome { - Some(cleanup_error) => format!( - "{message}. remote signer cleanup needs retry: {cleanup_error}" - ), - None => message, - }; - tracker.push_update(Err(error)); - tracker.finish_polling(request_generation); - return; - } - Err(error) => { - tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); - tracker.push_update(Err(error)); - tracker.finish_polling(request_generation); - return; - } - } - } - }); - } - - fn push_update(&self, result: Result<Option<H::ReadyState>, String>) { - if let Ok(mut slot) = self.update.lock() { - *slot = Some(result); - self.changed.store(true, Ordering::Release); - } - } - - fn apply_progress(&self, update: RadrootsAppRemoteSignerProgressUpdate) { - match update { - RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url } => { - let next = - RadrootsAppRemoteSignerPendingState::AwaitingAuthorization { url: url.clone() }; - if self.set_pending_state(next) { - self.push_update(Err(format!( - "authorize the remote signer to continue: {url}" - ))); - } - } - } - } - - fn set_pending_state(&self, next: RadrootsAppRemoteSignerPendingState) -> bool { - if let Ok(mut state) = self.pending_state.lock() { - if *state == next { - return false; - } - *state = next; - return true; - } - false - } - - fn finish_polling(&self, worker_generation: u64) { - self.polling.store(false, Ordering::Release); - if self.poll_generation.load(Ordering::Acquire) != worker_generation { - self.start_polling(); - } - } -} - -fn default_connect_pending(input: &str) -> Result<RadrootsAppRemoteSignerPendingSession, String> { - radroots_app_remote_signer_connect_pending(input).map_err(|error| error.to_string()) -} - -fn default_poll_pending() -> RadrootsAppRemoteSignerPollPendingFn { - let cache: Arc<Mutex<Option<(String, RadrootsAppRemoteSignerPendingPoller)>>> = - Arc::new(Mutex::new(None)); - Arc::new( - move |record, client_secret_key_hex, progress| -> Result<_, String> { - let client_account_id = record.client_account_id().to_owned(); - let mut cache = cache - .lock() - .map_err(|_| "pending poller cache lock poisoned".to_owned())?; - let poller = match cache.as_mut() { - Some((cached_account_id, poller)) if *cached_account_id == client_account_id => { - poller - } - _ => { - let poller = radroots_app_remote_signer_open_pending_poller( - record, - client_secret_key_hex, - ) - .map_err(|error| error.to_string())?; - *cache = Some((client_account_id.clone(), poller)); - &mut cache.as_mut().expect("cache initialized").1 - } - }; - let outcome = radroots_app_remote_signer_poll_pending_poller_with_progress( - poller, - &mut |update| progress(update), - ) - .map_err(|error| error.to_string())?; - if !matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval - | RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { .. } - ) { - *cache = None; - } - Ok(outcome) - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, fixture_identity}; - use radroots_identity::RadrootsIdentityPublic; - use std::collections::{HashMap, VecDeque}; - use std::sync::Condvar; - use std::sync::mpsc::{self, Receiver, Sender}; - use std::time::Instant; - - #[derive(Clone, Debug)] - enum TestPendingBehavior { - PendingApproval, - TransportFailure(&'static str), - Rejected(&'static str), - } - - #[derive(Default)] - struct TestHooksState { - pending: Option<RadrootsAppRemoteSignerSessionRecord>, - active: HashMap<String, String>, - secrets: HashMap<String, String>, - pending_record_gate: Option<PendingRecordGate>, - clear_pending_gate: Option<ClearPendingGate>, - } - - #[derive(Clone)] - struct PendingRecordGate { - entered: Sender<()>, - release: Arc<(Mutex<bool>, Condvar)>, - } - - #[derive(Clone)] - struct ClearPendingGate { - entered: Sender<()>, - release: Arc<(Mutex<bool>, Condvar)>, - } - - #[derive(Clone, Default)] - struct TestHooks { - state: Arc<Mutex<TestHooksState>>, - } - - impl TestHooks { - fn set_pending(&self, record: Option<RadrootsAppRemoteSignerSessionRecord>) { - self.state.lock().expect("hooks lock").pending = record; - } - - fn set_secret(&self, client_account_id: &str, secret: &str) { - self.state - .lock() - .expect("hooks lock") - .secrets - .insert(client_account_id.to_owned(), secret.to_owned()); - } - - fn install_pending_record_gate( - &self, - entered: Sender<()>, - release: Arc<(Mutex<bool>, Condvar)>, - ) { - self.state.lock().expect("hooks lock").pending_record_gate = - Some(PendingRecordGate { entered, release }); - } - - fn install_clear_pending_gate( - &self, - entered: Sender<()>, - release: Arc<(Mutex<bool>, Condvar)>, - ) { - self.state.lock().expect("hooks lock").clear_pending_gate = - Some(ClearPendingGate { entered, release }); - } - } - - impl RadrootsAppRemoteSignerControllerHooks for TestHooks { - type ReadyState = String; - - fn store_pending_session( - &self, - pending: &RadrootsAppRemoteSignerPendingSession, - ) -> Result<(), String> { - let mut state = self - .state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned())?; - state.pending = Some(pending.record.clone()); - state.secrets.insert( - pending.record.client_account_id().to_owned(), - pending.client_secret_key_hex.clone(), - ); - Ok(()) - } - - fn pending_session_record( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let gate = { - self.state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned())? - .pending_record_gate - .take() - }; - if let Some(gate) = gate { - let _ = gate.entered.send(()); - wait_for_gate(&gate.release); - } - self.state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned()) - .map(|state| state.pending.clone()) - } - - fn load_pending_client_secret(&self, client_account_id: &str) -> Result<String, String> { - self.state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned())? - .secrets - .get(client_account_id) - .cloned() - .ok_or_else(|| "missing pending client secret".to_owned()) - } - - fn activate_pending_session( - &self, - client_account_id: &str, - approved: RadrootsAppRemoteSignerApprovedSession, - ) -> Result<Self::ReadyState, String> { - let mut state = self - .state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned())?; - state.pending = None; - state.active.insert( - client_account_id.to_owned(), - approved.user_identity.id.to_string(), - ); - Ok(approved.user_identity.id.to_string()) - } - - fn clear_pending_session( - &self, - ) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let (removed, gate) = { - let mut state = self - .state - .lock() - .map_err(|_| "hooks lock poisoned".to_owned())?; - (state.pending.take(), state.clear_pending_gate.take()) - }; - if let Some(gate) = gate { - let _ = gate.entered.send(()); - wait_for_gate(&gate.release); - } - Ok(removed) - } - } - - fn wait_for_gate(gate: &Arc<(Mutex<bool>, Condvar)>) { - let (ready_lock, ready_cvar) = &**gate; - let mut ready = ready_lock.lock().expect("gate lock"); - while !*ready { - ready = ready_cvar.wait(ready).expect("gate wait"); - } - } - - fn open_gate(gate: &Arc<(Mutex<bool>, Condvar)>) { - let (ready_lock, ready_cvar) = &**gate; - let mut ready = ready_lock.lock().expect("gate lock"); - *ready = true; - ready_cvar.notify_all(); - } - - fn fixture_public( - fixture: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity, - ) -> RadrootsIdentityPublic { - fixture_identity(fixture).expect("identity").to_public() - } - - fn pending_record(client: &str, signer: &str) -> RadrootsAppRemoteSignerSessionRecord { - RadrootsAppRemoteSignerSessionRecord::pending( - fixture_public(match client { - "alice-client" => &FIXTURE_ALICE, - "bob-client" => &FIXTURE_BOB, - _ => &FIXTURE_CAROL, - }), - fixture_public(match signer { - "alice-signer" => &FIXTURE_ALICE, - "bob-signer" => &FIXTURE_BOB, - _ => &FIXTURE_CAROL, - }), - vec!["wss://relay.example".to_owned()], - ) - } - - fn pending_session_for_input( - input: &str, - ) -> Result<RadrootsAppRemoteSignerPendingSession, String> { - let record = match input { - "next" => pending_record("bob-client", "bob-signer"), - "reject-next" => pending_record("carol-client", "carol-signer"), - other => return Err(format!("unexpected connect input: {other}")), - }; - Ok(RadrootsAppRemoteSignerPendingSession { - client_secret_key_hex: format!("secret-for-{}", record.client_account_id()), - record, - }) - } - - fn no_sleep(_: Duration) {} - - fn wait_for_message(receiver: &Receiver<String>) -> String { - receiver - .recv_timeout(Duration::from_secs(2)) - .expect("timed out waiting for poll message") - } - - fn wait_for_update( - controller: &RadrootsAppRemoteSignerController<TestHooks>, - ) -> Result<Option<String>, String> { - let deadline = Instant::now() + Duration::from_secs(2); - loop { - if let Some(update) = controller.take_update() { - return update; - } - if Instant::now() >= deadline { - panic!("timed out waiting for controller update"); - } - std::thread::sleep(Duration::from_millis(10)); - } - } - - #[test] - fn restart_request_during_empty_exit_window_respawns_poller() { - let hooks = TestHooks::default(); - let (poll_tx, poll_rx) = mpsc::channel(); - - let controller = RadrootsAppRemoteSignerController::new_with_ops( - hooks.clone(), - Arc::new(pending_session_for_input), - Arc::new(move |record, _, _progress| { - poll_tx - .send(record.client_account_id().to_owned()) - .expect("send poll id"); - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { - message: "rejected".to_owned(), - }) - }), - Arc::new(no_sleep), - ); - - let initial = pending_record("alice-client", "alice-signer"); - hooks.set_secret(initial.client_account_id(), "secret-for-initial"); - hooks.set_pending(Some(initial.clone())); - let (entered_tx, entered_rx) = mpsc::channel(); - let release = Arc::new((Mutex::new(false), Condvar::new())); - hooks.install_pending_record_gate(entered_tx, Arc::clone(&release)); - controller.start_polling(); - - hooks.set_pending(None); - entered_rx - .recv_timeout(Duration::from_secs(2)) - .expect("pending record gate was not entered"); - - let next = pending_session_for_input("next").expect("next pending"); - hooks.set_secret(next.record.client_account_id(), "secret-for-next"); - hooks.set_pending(Some(next.record.clone())); - controller.start_polling(); - open_gate(&release); - - assert_eq!(wait_for_message(&poll_rx), next.record.client_account_id()); - } - - #[test] - fn begin_connect_after_pending_clear_restarts_polling() { - let hooks = TestHooks::default(); - let (poll_tx, poll_rx) = mpsc::channel(); - - let controller = RadrootsAppRemoteSignerController::new_with_ops( - hooks.clone(), - Arc::new(pending_session_for_input), - Arc::new(move |record, _, _progress| { - poll_tx - .send(record.client_account_id().to_owned()) - .expect("send poll id"); - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { - message: "rejected".to_owned(), - }) - }), - Arc::new(no_sleep), - ); - - let initial = pending_record("alice-client", "alice-signer"); - hooks.set_secret(initial.client_account_id(), "secret-for-initial"); - hooks.set_pending(Some(initial)); - let (entered_tx, entered_rx) = mpsc::channel(); - let release = Arc::new((Mutex::new(false), Condvar::new())); - hooks.install_pending_record_gate(entered_tx, Arc::clone(&release)); - controller.start_polling(); - - hooks.set_pending(None); - entered_rx - .recv_timeout(Duration::from_secs(2)) - .expect("pending record gate was not entered"); - - controller.begin_connect("next").expect("begin connect"); - open_gate(&release); - - let expected = pending_session_for_input("next") - .expect("next pending") - .record - .client_account_id() - .to_owned(); - assert_eq!(wait_for_message(&poll_rx), expected); - } - - #[test] - fn begin_connect_after_rejection_cleanup_restarts_polling() { - let hooks = TestHooks::default(); - let (poll_tx, poll_rx) = mpsc::channel(); - - let controller = RadrootsAppRemoteSignerController::new_with_ops( - hooks.clone(), - Arc::new(pending_session_for_input), - Arc::new(move |record, _, _progress| { - poll_tx - .send(record.client_account_id().to_owned()) - .expect("send poll id"); - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { - message: "rejected".to_owned(), - }) - }), - Arc::new(no_sleep), - ); - - let initial = pending_record("alice-client", "alice-signer"); - hooks.set_secret(initial.client_account_id(), "secret-for-initial"); - hooks.set_pending(Some(initial)); - let (clear_tx, clear_rx) = mpsc::channel(); - let release = Arc::new((Mutex::new(false), Condvar::new())); - hooks.install_clear_pending_gate(clear_tx, Arc::clone(&release)); - controller.start_polling(); - - assert_eq!( - wait_for_message(&poll_rx), - fixture_public(&FIXTURE_ALICE).id.to_string() - ); - clear_rx - .recv_timeout(Duration::from_secs(2)) - .expect("clear pending gate was not entered"); - - controller - .begin_connect("reject-next") - .expect("begin connect after rejection"); - open_gate(&release); - - let expected = pending_session_for_input("reject-next") - .expect("reject-next pending") - .record - .client_account_id() - .to_owned(); - assert_eq!(wait_for_message(&poll_rx), expected); - } - - #[test] - fn transport_failure_recovers_back_to_waiting_approval() { - let hooks = TestHooks::default(); - let pending = pending_record("alice-client", "alice-signer"); - hooks.set_secret(pending.client_account_id(), "secret-for-initial"); - hooks.set_pending(Some(pending.clone())); - let outcomes = Arc::new(Mutex::new(VecDeque::from([ - TestPendingBehavior::TransportFailure("relay down"), - TestPendingBehavior::PendingApproval, - TestPendingBehavior::Rejected("done"), - ]))); - let (sleep_enter_tx, sleep_enter_rx) = mpsc::channel(); - let first_sleep_release = Arc::new((Mutex::new(false), Condvar::new())); - let second_sleep_release = Arc::new((Mutex::new(false), Condvar::new())); - let first_sleep_release_for_closure = Arc::clone(&first_sleep_release); - let second_sleep_release_for_closure = Arc::clone(&second_sleep_release); - let sleep_tick = Arc::new(AtomicU64::new(0)); - let sleep_tick_for_closure = Arc::clone(&sleep_tick); - - let controller = RadrootsAppRemoteSignerController::new_with_ops( - hooks.clone(), - Arc::new(pending_session_for_input), - Arc::new(move |_, _, _progress| { - let next = outcomes - .lock() - .expect("outcomes lock") - .pop_front() - .expect("missing test outcome"); - match next { - TestPendingBehavior::PendingApproval => { - Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) - } - TestPendingBehavior::TransportFailure(message) => Ok( - RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { - message: message.to_owned(), - }, - ), - TestPendingBehavior::Rejected(message) => { - Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { - message: message.to_owned(), - }) - } - } - }), - Arc::new(move |_| { - let tick = sleep_tick_for_closure.fetch_add(1, Ordering::AcqRel) + 1; - let _ = sleep_enter_tx.send(tick); - match tick { - 1 => wait_for_gate(&first_sleep_release_for_closure), - 2 => wait_for_gate(&second_sleep_release_for_closure), - _ => {} - } - }), - ); - - let update = wait_for_update(&controller).expect_err("transport failure update"); - assert_eq!(update, "remote signer approval check failed: relay down"); - assert_eq!( - sleep_enter_rx - .recv_timeout(Duration::from_secs(2)) - .expect("transport retry sleep"), - 1 - ); - open_gate(&first_sleep_release); - - assert_eq!( - sleep_enter_rx - .recv_timeout(Duration::from_secs(2)) - .expect("pending approval sleep"), - 2 - ); - assert_eq!( - controller.pending_state(), - RadrootsAppRemoteSignerPendingState::WaitingApproval - ); - open_gate(&second_sleep_release); - } -} diff --git a/crates/shared/remote_signer/src/custody.rs b/crates/shared/remote_signer/src/custody.rs @@ -1,624 +0,0 @@ -use crate::session::{ - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStatus, - RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerSessionStoreState, -}; -use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, -}; -use std::collections::HashSet; -use std::path::Path; - -pub fn radroots_app_remote_signer_clear_pending_session( - path: &Path, - remove_client_secret: impl Fn(&str) -> Result<(), String>, -) -> Result<Option<RadrootsAppRemoteSignerSessionRecord>, String> { - let state = load_sessions(path)?; - let Some(record) = state.pending_session().cloned() else { - return Ok(None); - }; - let mut next_state = state.clone(); - let removed = next_state.remove_pending_session(); - if removed.is_none() { - return Err("remote signer pending session record cleanup could not complete".to_owned()); - } - save_sessions(path, &next_state)?; - - if let Err(error) = remove_client_secret(record.client_account_id()) { - return Err(format!( - "remote signer pending session record was removed but session secret cleanup needs retry: {error}" - )); - } - - Ok(removed) -} - -pub fn radroots_app_remote_signer_disconnect_selected( - manager: &RadrootsNostrAccountsManager, - path: &Path, - remove_client_secret: impl Fn(&str) -> Result<(), String>, -) -> Result<RadrootsNostrSelectedAccountStatus, String> { - let Some(account_id) = manager - .selected_account_id() - .map_err(|source| source.to_string())? - else { - return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured); - }; - - let state = load_sessions(path)?; - let Some(session) = state - .active_session_for_account_id(account_id.as_str()) - .cloned() - else { - return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured); - }; - - let mut next_state = state.clone(); - let removed = next_state.remove_active_session_for_account_id(account_id.as_str()); - if removed.is_none() { - return Err("remote signer session record cleanup could not complete".to_owned()); - } - save_sessions(path, &next_state)?; - - if let Err(error) = manager.remove_account(&account_id) { - if let Err(rollback_error) = save_sessions(path, &state) { - return Err(format!( - "failed to remove remote signer account: {error}. session rollback also failed: {rollback_error}" - )); - } - return Err(error.to_string()); - } - - if let Err(error) = remove_client_secret(session.client_account_id()) { - return Err(format!( - "remote signer account and session were removed but session secret cleanup needs retry: {error}" - )); - } - - manager - .selected_account_status() - .map_err(|source| source.to_string()) -} - -pub fn radroots_app_remote_signer_reconcile_startup( - manager: &RadrootsNostrAccountsManager, - path: &Path, - remote_signer_label: &str, - load_client_secret: impl Fn(&str) -> Result<String, String>, - remove_client_secret: impl Fn(&str) -> Result<(), String>, - purge_client_secret_namespace: impl Fn() -> Result<(), String>, -) -> Result<(), String> { - let load = load_sessions_with_recovery(path)?; - let mut state = load.state; - let mut dirty = false; - let accounts = manager - .list_accounts() - .map_err(|source| source.to_string())?; - let account_ids = accounts - .iter() - .map(|record| record.account_id.to_string()) - .collect::<HashSet<_>>(); - let active_session_account_ids = state - .sessions - .iter() - .filter(|record| record.status == RadrootsAppRemoteSignerSessionStatus::Active) - .filter_map(|record| record.account_id().map(ToOwned::to_owned)) - .collect::<HashSet<_>>(); - - let should_purge_namespace = load.recovered_from_corruption || state.sessions.is_empty(); - - if should_purge_namespace { - purge_client_secret_namespace()?; - } - - for account in remote_signer_public_only_accounts(manager, &accounts, remote_signer_label)? - .into_iter() - .filter(|account| !active_session_account_ids.contains(account.account_id.as_str())) - { - manager - .remove_account(&account.account_id) - .map_err(|source| source.to_string())?; - } - - if let Some(record) = state.pending_session().cloned() { - if load_client_secret(record.client_account_id()).is_err() { - state.remove_pending_session(); - dirty = true; - } - } - - let stale_active_sessions = state - .sessions - .iter() - .filter(|record| record.status == RadrootsAppRemoteSignerSessionStatus::Active) - .filter_map(|record| { - let account_id = record.account_id()?; - (!account_ids.contains(account_id)).then_some(record.clone()) - }) - .collect::<Vec<_>>(); - - for session in stale_active_sessions { - remove_client_secret(session.client_account_id())?; - let Some(account_id) = session.account_id() else { - continue; - }; - state.remove_active_session_for_account_id(account_id); - dirty = true; - } - - if dirty || load.recovered_from_corruption { - save_sessions(path, &state)?; - } - - Ok(()) -} - -pub fn radroots_app_remote_signer_purge_all_custody_state( - path: &Path, - remove_client_secret: impl Fn(&str) -> Result<(), String>, - purge_client_secret_namespace: impl Fn() -> Result<(), String>, -) -> Result<(), String> { - let load = load_sessions_with_recovery(path)?; - for record in &load.state.sessions { - remove_client_secret(record.client_account_id())?; - } - purge_client_secret_namespace()?; - remove_sessions_file_if_present(path)?; - Ok(()) -} - -fn remote_signer_public_only_accounts( - manager: &RadrootsNostrAccountsManager, - accounts: &[RadrootsNostrAccountRecord], - remote_signer_label: &str, -) -> Result<Vec<RadrootsNostrAccountRecord>, String> { - let mut stale = Vec::new(); - for account in accounts { - if account.label.as_deref() != Some(remote_signer_label) { - continue; - } - if manager - .get_signing_identity(&account.account_id) - .map_err(|source| source.to_string())? - .is_none() - { - stale.push(account.clone()); - } - } - Ok(stale) -} - -fn load_sessions(path: &Path) -> Result<RadrootsAppRemoteSignerSessionStoreState, String> { - RadrootsAppRemoteSignerSessionStoreState::load(path).map_err(|error| error.to_string()) -} - -fn load_sessions_with_recovery( - path: &Path, -) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, String> { - RadrootsAppRemoteSignerSessionStoreState::load_with_recovery(path) - .map_err(|error| error.to_string()) -} - -fn save_sessions( - path: &Path, - state: &RadrootsAppRemoteSignerSessionStoreState, -) -> Result<(), String> { - state.save(path).map_err(|error| error.to_string()) -} - -fn remove_sessions_file_if_present(path: &Path) -> Result<(), String> { - match std::fs::remove_file(path) { - Ok(()) => Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(format!( - "failed to remove remote signer session store: {error}" - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, fixture_identity}; - use radroots_identity::RadrootsIdentityId; - use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountsManager, RadrootsNostrSecretVaultMemory, - RadrootsNostrSelectedAccountStatus, RadrootsSecretVault, account_secret_slot, - }; - - const REMOTE_SIGNER_LABEL: &str = "remote signer"; - - fn fixture_public( - label: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity, - ) -> radroots_identity::RadrootsIdentityPublic { - fixture_identity(label).expect("identity").to_public() - } - - fn fixture_account_id(value: &str) -> RadrootsIdentityId { - RadrootsIdentityId::try_from(value).expect("account id") - } - - fn secret_store_secret( - vault: &RadrootsNostrSecretVaultMemory, - client_account_id: &str, - secret: &str, - ) { - let slot = account_secret_slot(&fixture_account_id(client_account_id)); - vault - .store_secret(slot.as_str(), secret) - .expect("store secret"); - } - - fn secret_loader( - vault: RadrootsNostrSecretVaultMemory, - ) -> impl Fn(&str) -> Result<String, String> { - move |client_account_id| { - let slot = account_secret_slot(&fixture_account_id(client_account_id)); - vault - .load_secret(slot.as_str()) - .map_err(|source| source.to_string())? - .ok_or_else(|| "missing secret".to_owned()) - } - } - - fn secret_remover( - vault: RadrootsNostrSecretVaultMemory, - ) -> impl Fn(&str) -> Result<(), String> { - move |client_account_id| { - let slot = account_secret_slot(&fixture_account_id(client_account_id)); - vault - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string()) - } - } - - fn secret_namespace_purger( - vault: RadrootsNostrSecretVaultMemory, - client_account_ids: Vec<String>, - ) -> impl Fn() -> Result<(), String> { - move || { - for client_account_id in &client_account_ids { - let slot = account_secret_slot(&fixture_account_id(client_account_id)); - vault - .remove_secret(slot.as_str()) - .map_err(|source| source.to_string())?; - } - Ok(()) - } - } - - fn write_pending_state(path: &Path) -> RadrootsAppRemoteSignerSessionRecord { - let record = RadrootsAppRemoteSignerSessionRecord::pending( - fixture_public(&FIXTURE_ALICE), - fixture_public(&FIXTURE_BOB), - vec!["wss://relay.example.com".to_owned()], - ); - let mut state = RadrootsAppRemoteSignerSessionStoreState::default(); - state.upsert_pending(record.clone()).expect("pending"); - state.save(path).expect("save"); - record - } - - fn write_active_state(path: &Path) -> RadrootsAppRemoteSignerSessionRecord { - let user_identity = fixture_public(&FIXTURE_CAROL); - let mut record = RadrootsAppRemoteSignerSessionRecord::pending( - fixture_public(&FIXTURE_ALICE), - fixture_public(&FIXTURE_BOB), - vec!["wss://relay.example.com".to_owned()], - ); - record.user_identity = Some(user_identity); - record.status = RadrootsAppRemoteSignerSessionStatus::Active; - let mut state = RadrootsAppRemoteSignerSessionStoreState::default(); - state.sessions.push(record.clone()); - state.save(path).expect("save"); - record - } - - #[test] - fn clear_pending_session_removes_secret_and_session_record() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - let record = write_pending_state(path.as_path()); - let vault = RadrootsNostrSecretVaultMemory::new(); - secret_store_secret(&vault, record.client_account_id(), "deadbeef"); - - let removed = radroots_app_remote_signer_clear_pending_session( - path.as_path(), - secret_remover(vault.clone()), - ) - .expect("clear pending"); - - assert_eq!( - removed.expect("removed").client_account_id(), - record.client_account_id() - ); - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(record.client_account_id())).as_str() - ) - .expect("load") - .is_none() - ); - assert!( - RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()) - .expect("load") - .sessions - .is_empty() - ); - } - - #[test] - fn clear_pending_session_leaves_secret_for_retry_when_secret_cleanup_fails() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - let record = write_pending_state(path.as_path()); - let vault = RadrootsNostrSecretVaultMemory::new(); - secret_store_secret(&vault, record.client_account_id(), "deadbeef"); - - let error = radroots_app_remote_signer_clear_pending_session( - path.as_path(), - |_client_account_id| Err("vault unavailable".to_owned()), - ) - .expect_err("cleanup retry"); - - assert!(error.contains("session secret cleanup needs retry")); - assert!( - RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()) - .expect("load") - .sessions - .is_empty() - ); - assert_eq!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(record.client_account_id())).as_str() - ) - .expect("load retained secret") - .as_deref(), - Some("deadbeef") - ); - } - - #[test] - fn disconnect_selected_remote_signer_leaves_session_for_retry_when_secret_cleanup_fails() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - let record = write_active_state(path.as_path()); - let manager = RadrootsNostrAccountsManager::new_in_memory(); - manager - .upsert_public_identity( - record.user_identity.clone().expect("user"), - Some(REMOTE_SIGNER_LABEL.to_owned()), - true, - ) - .expect("upsert"); - - let error = radroots_app_remote_signer_disconnect_selected( - &manager, - path.as_path(), - |_client_account_id| Err("vault unavailable".to_owned()), - ) - .expect_err("cleanup failure"); - - assert!(error.contains("session secret cleanup needs retry")); - assert!(matches!( - manager.selected_account_status().expect("status"), - RadrootsNostrSelectedAccountStatus::NotConfigured - )); - assert!( - RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()) - .expect("load") - .active_session_for_account_id( - record - .account_id() - .expect("account id after disconnect failure") - ) - .is_none() - ); - } - - #[test] - fn reconcile_startup_removes_remote_signer_public_only_accounts_after_store_quarantine() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - std::fs::write(path.as_path(), "{invalid").expect("write invalid"); - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let public = fixture_public(&FIXTURE_CAROL); - let account_id = public.id.clone(); - manager - .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true) - .expect("upsert"); - - radroots_app_remote_signer_reconcile_startup( - &manager, - path.as_path(), - REMOTE_SIGNER_LABEL, - secret_loader(RadrootsNostrSecretVaultMemory::new()), - secret_remover(RadrootsNostrSecretVaultMemory::new()), - secret_namespace_purger(RadrootsNostrSecretVaultMemory::new(), Vec::new()), - ) - .expect("reconcile"); - - assert!( - manager - .list_accounts() - .expect("accounts") - .iter() - .all(|record| record.account_id != account_id) - ); - assert!( - RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()) - .expect("load") - .sessions - .is_empty() - ); - } - - #[test] - fn reconcile_startup_removes_orphan_remote_signer_public_only_accounts_without_corruption() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - RadrootsAppRemoteSignerSessionStoreState::default() - .save(path.as_path()) - .expect("save empty"); - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let public = fixture_public(&FIXTURE_CAROL); - let account_id = public.id.clone(); - manager - .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true) - .expect("upsert"); - - radroots_app_remote_signer_reconcile_startup( - &manager, - path.as_path(), - REMOTE_SIGNER_LABEL, - secret_loader(RadrootsNostrSecretVaultMemory::new()), - secret_remover(RadrootsNostrSecretVaultMemory::new()), - secret_namespace_purger(RadrootsNostrSecretVaultMemory::new(), Vec::new()), - ) - .expect("reconcile orphan account"); - - assert!( - manager - .list_accounts() - .expect("accounts") - .iter() - .all(|record| record.account_id != account_id) - ); - } - - #[test] - fn purge_all_custody_state_removes_all_tracked_client_secrets_and_session_file() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - let pending = write_pending_state(path.as_path()); - let mut active = write_active_state(path.as_path()); - active.client_identity = fixture_public(&FIXTURE_BOB); - let mut state = - RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load"); - state.sessions.push(active.clone()); - state.save(path.as_path()).expect("save"); - - let vault = RadrootsNostrSecretVaultMemory::new(); - secret_store_secret(&vault, pending.client_account_id(), "pending"); - secret_store_secret(&vault, active.client_account_id(), "active"); - - radroots_app_remote_signer_purge_all_custody_state( - path.as_path(), - secret_remover(vault.clone()), - secret_namespace_purger( - vault.clone(), - vec![ - pending.client_account_id().to_owned(), - active.client_account_id().to_owned(), - ], - ), - ) - .expect("purge"); - - assert!(!path.exists()); - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(pending.client_account_id())).as_str() - ) - .expect("pending removed") - .is_none() - ); - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(active.client_account_id())).as_str() - ) - .expect("active removed") - .is_none() - ); - } - - #[test] - fn reconcile_startup_purges_namespace_after_store_quarantine() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - std::fs::write(path.as_path(), "{invalid").expect("write invalid"); - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let alice_client_account_id = fixture_public(&FIXTURE_ALICE).id; - let bob_client_account_id = fixture_public(&FIXTURE_BOB).id; - let public = fixture_public(&FIXTURE_CAROL); - manager - .upsert_public_identity(public, Some(REMOTE_SIGNER_LABEL.to_owned()), true) - .expect("upsert"); - - let vault = RadrootsNostrSecretVaultMemory::new(); - secret_store_secret(&vault, alice_client_account_id.as_str(), "pending"); - secret_store_secret(&vault, bob_client_account_id.as_str(), "active"); - - radroots_app_remote_signer_reconcile_startup( - &manager, - path.as_path(), - REMOTE_SIGNER_LABEL, - secret_loader(vault.clone()), - secret_remover(vault.clone()), - secret_namespace_purger( - vault.clone(), - vec![ - alice_client_account_id.to_string(), - bob_client_account_id.to_string(), - ], - ), - ) - .expect("reconcile after quarantine"); - - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(alice_client_account_id.as_str())) - .as_str() - ) - .expect("pending removed by namespace purge") - .is_none() - ); - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(bob_client_account_id.as_str())) - .as_str() - ) - .expect("active removed by namespace purge") - .is_none() - ); - } - - #[test] - fn reconcile_startup_purges_namespace_when_session_store_is_empty() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - RadrootsAppRemoteSignerSessionStoreState::default() - .save(path.as_path()) - .expect("save empty"); - let manager = RadrootsNostrAccountsManager::new_in_memory(); - let alice_client_account_id = fixture_public(&FIXTURE_ALICE).id; - - let vault = RadrootsNostrSecretVaultMemory::new(); - secret_store_secret(&vault, alice_client_account_id.as_str(), "pending"); - - radroots_app_remote_signer_reconcile_startup( - &manager, - path.as_path(), - REMOTE_SIGNER_LABEL, - secret_loader(vault.clone()), - secret_remover(vault.clone()), - secret_namespace_purger(vault.clone(), vec![alice_client_account_id.to_string()]), - ) - .expect("reconcile empty store"); - - assert!( - vault - .load_secret( - account_secret_slot(&fixture_account_id(alice_client_account_id.as_str())) - .as_str() - ) - .expect("pending removed by empty-store namespace purge") - .is_none() - ); - } -} diff --git a/crates/shared/remote_signer/src/error.rs b/crates/shared/remote_signer/src/error.rs @@ -1,64 +0,0 @@ -use radroots_nostr_connect::prelude::{RadrootsNostrConnectError, RadrootsNostrConnectMethod}; -use std::fmt; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsAppRemoteSignerError { - EmptyInput, - UnsupportedClientUri, - MissingDiscoveryUri, - InvalidDiscoveryUrl(String), - InvalidBunkerUri(String), - InvalidSessionStore(String), - SessionStoreIo(String), - PendingSessionExists, - MissingClientSecret, - ConnectFailed(String), - RequestTimedOut { - method: RadrootsNostrConnectMethod, - }, - UnexpectedResponse { - method: RadrootsNostrConnectMethod, - response: String, - }, -} - -impl fmt::Display for RadrootsAppRemoteSignerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::EmptyInput => f.write_str("enter a bunker or discovery url to continue"), - Self::UnsupportedClientUri => f.write_str( - "enter a bunker or discovery url from the signer; raw nostrconnect client uris are signer-side only", - ), - Self::MissingDiscoveryUri => { - f.write_str("discovery url does not contain a remote signer uri") - } - Self::InvalidDiscoveryUrl(reason) => { - write!(f, "invalid discovery url: {reason}") - } - Self::InvalidBunkerUri(reason) => { - write!(f, "invalid remote signer uri: {reason}") - } - Self::InvalidSessionStore(reason) => write!(f, "invalid remote signer store: {reason}"), - Self::SessionStoreIo(reason) => write!(f, "remote signer storage failed: {reason}"), - Self::PendingSessionExists => { - f.write_str("a remote signer connection is already pending approval") - } - Self::MissingClientSecret => f.write_str("remote signer session secret is missing"), - Self::ConnectFailed(reason) => write!(f, "remote signer connection failed: {reason}"), - Self::RequestTimedOut { method } => { - write!(f, "remote signer request `{method}` timed out") - } - Self::UnexpectedResponse { method, response } => { - write!(f, "remote signer returned an unexpected `{method}` response: {response}") - } - } - } -} - -impl std::error::Error for RadrootsAppRemoteSignerError {} - -impl From<RadrootsNostrConnectError> for RadrootsAppRemoteSignerError { - fn from(value: RadrootsNostrConnectError) -> Self { - Self::InvalidBunkerUri(value.to_string()) - } -} diff --git a/crates/shared/remote_signer/src/input.rs b/crates/shared/remote_signer/src/input.rs @@ -1,168 +0,0 @@ -use crate::error::RadrootsAppRemoteSignerError; -use radroots_identity::RadrootsIdentityPublic; -use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, - RadrootsNostrConnectUri, -}; -use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME; -use url::Url; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RadrootsAppRemoteSignerSource { - BunkerUri, - DiscoveryUrl, -} - -#[derive(Debug, Clone)] -pub struct RadrootsAppRemoteSignerTarget { - pub source: RadrootsAppRemoteSignerSource, - pub signer_identity: RadrootsIdentityPublic, - pub relays: Vec<String>, - pub connect_secret: Option<String>, - pub requested_permissions: RadrootsNostrConnectPermissions, -} - -impl RadrootsAppRemoteSignerTarget { - pub fn source_label(&self) -> &'static str { - match self.source { - RadrootsAppRemoteSignerSource::BunkerUri => "bunker uri", - RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url", - } - } - - pub fn requested_permission_labels(&self) -> Vec<String> { - self.requested_permissions - .as_slice() - .iter() - .map(ToString::to_string) - .collect() - } -} - -pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions { - vec![ - RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "kind:1", - ), - RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), - ] - .into() -} - -pub fn radroots_app_remote_signer_preview( - input: &str, -) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { - let trimmed = input.trim(); - if trimmed.is_empty() { - return Err(RadrootsAppRemoteSignerError::EmptyInput); - } - - if trimmed.starts_with(&format!("{RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME}://")) { - return parse_bunker_uri(trimmed, RadrootsAppRemoteSignerSource::BunkerUri); - } - - if trimmed.starts_with("nostrconnect://") { - return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri); - } - - parse_discovery_url(trimmed) -} - -fn parse_discovery_url( - value: &str, -) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { - let url = Url::parse(value) - .map_err(|error| RadrootsAppRemoteSignerError::InvalidDiscoveryUrl(error.to_string()))?; - let Some((_, bunker_uri)) = url.query_pairs().find(|(key, _)| key == "uri") else { - return Err(RadrootsAppRemoteSignerError::MissingDiscoveryUri); - }; - parse_bunker_uri( - bunker_uri.as_ref(), - RadrootsAppRemoteSignerSource::DiscoveryUrl, - ) -} - -fn parse_bunker_uri( - value: &str, - source: RadrootsAppRemoteSignerSource, -) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { - let uri = RadrootsNostrConnectUri::parse(value)?; - let RadrootsNostrConnectUri::Bunker(bunker_uri) = uri else { - return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri); - }; - Ok(RadrootsAppRemoteSignerTarget { - source, - signer_identity: RadrootsIdentityPublic::new(bunker_uri.remote_signer_public_key), - relays: bunker_uri - .relays - .into_iter() - .map(|relay| relay.to_string()) - .collect(), - connect_secret: bunker_uri.secret, - requested_permissions: radroots_app_remote_signer_requested_permissions(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS}; - - fn bunker_uri() -> String { - format!( - "bunker://{}?relay={}", - FIXTURE_ALICE.npub, - urlencoding(RELAY_PRIMARY_WSS) - ) - } - - fn discovery_url() -> String { - format!( - "http://localhost/connect?uri={}", - urlencoding(bunker_uri().as_str()) - ) - } - - fn urlencoding(value: &str) -> String { - url::form_urlencoded::byte_serialize(value.as_bytes()).collect() - } - - #[test] - fn parses_direct_bunker_uri() { - let preview = radroots_app_remote_signer_preview(bunker_uri().as_str()).expect("preview"); - - assert_eq!(preview.source, RadrootsAppRemoteSignerSource::BunkerUri); - assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub); - assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); - assert_eq!(preview.connect_secret, None); - assert_eq!( - preview.requested_permission_labels(), - vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()] - ); - } - - #[test] - fn parses_discovery_url_with_bunker_uri() { - let preview = - radroots_app_remote_signer_preview(discovery_url().as_str()).expect("preview"); - - assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl); - assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub); - assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); - assert_eq!( - preview.requested_permission_labels(), - vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()] - ); - } - - #[test] - fn rejects_client_side_nostrconnect_uri_input() { - let err = radroots_app_remote_signer_preview( - "nostrconnect://npub1test?relay=wss%3A%2F%2Frelay.example.com&secret=test", - ) - .expect_err("client uri rejected"); - - assert_eq!(err, RadrootsAppRemoteSignerError::UnsupportedClientUri); - } -} diff --git a/crates/shared/remote_signer/src/lib.rs b/crates/shared/remote_signer/src/lib.rs @@ -1,47 +0,0 @@ -#![forbid(unsafe_code)] - -mod action; -mod controller; -mod custody; -mod error; -mod input; -mod protocol; -mod session; - -pub const RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE: &str = "remote-signer"; - -pub use action::{ - RadrootsAppRemoteSignerActionController, RadrootsAppRemoteSignerActionControllerHooks, - RadrootsAppRemoteSignerActionState, -}; -pub use controller::{ - RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, - RadrootsAppRemoteSignerPendingState, -}; -pub use custody::{ - radroots_app_remote_signer_clear_pending_session, - radroots_app_remote_signer_disconnect_selected, - radroots_app_remote_signer_purge_all_custody_state, - radroots_app_remote_signer_reconcile_startup, -}; -pub use error::RadrootsAppRemoteSignerError; -pub use input::{ - RadrootsAppRemoteSignerSource, RadrootsAppRemoteSignerTarget, - radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions, -}; -pub use protocol::{ - RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, - RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerProgressUpdate, - RadrootsAppRemoteSignerSignedEvent, radroots_app_remote_signer_connect_pending, - radroots_app_remote_signer_poll_pending_session, - radroots_app_remote_signer_poll_pending_session_with_progress, - radroots_app_remote_signer_sign_kind1_note, - radroots_app_remote_signer_sign_kind1_note_with_progress, - radroots_app_remote_signer_sign_unsigned_event, - radroots_app_remote_signer_sign_unsigned_event_with_progress, -}; -pub use session::{ - RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION, RadrootsAppRemoteSignerSessionRecord, - RadrootsAppRemoteSignerSessionStatus, RadrootsAppRemoteSignerSessionStoreLoadResult, - RadrootsAppRemoteSignerSessionStoreState, -}; diff --git a/crates/shared/remote_signer/src/protocol.rs b/crates/shared/remote_signer/src/protocol.rs @@ -1,789 +0,0 @@ -use crate::error::RadrootsAppRemoteSignerError; -use crate::input::{RadrootsAppRemoteSignerTarget, radroots_app_remote_signer_preview}; -use crate::session::RadrootsAppRemoteSignerSessionRecord; -use nostr::JsonUtil; -use nostr::nips::nip44; -use nostr::nips::nip44::Version; -use nostr::{EventBuilder, UnsignedEvent}; -use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, - RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrTag, - RadrootsNostrTimestamp, radroots_nostr_filter_tag, radroots_nostr_kind, -}; -use radroots_nostr_connect::message::RADROOTS_NOSTR_CONNECT_RPC_KIND; -use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPendingConnectionPollOutcome, - RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, - RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, - RadrootsNostrConnectResponseEnvelope, -}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Duration; -use tokio::runtime::Builder; -use tokio::sync::broadcast; -use tokio::time::timeout; - -const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -const GET_SESSION_CAPABILITY_TIMEOUT: Duration = Duration::from_secs(60); -const SWITCH_RELAYS_TIMEOUT: Duration = Duration::from_secs(30); -const SIGN_EVENT_TIMEOUT: Duration = Duration::from_secs(60); -static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1); - -#[derive(Debug, Clone)] -pub struct RadrootsAppRemoteSignerPendingSession { - pub record: RadrootsAppRemoteSignerSessionRecord, - pub client_secret_key_hex: String, -} - -#[derive(Debug, Clone)] -pub struct RadrootsAppRemoteSignerApprovedSession { - pub user_identity: RadrootsIdentityPublic, - pub relays: Vec<String>, - pub approved_permissions: RadrootsNostrConnectPermissions, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsAppRemoteSignerSignedEvent { - pub event_id_hex: String, - pub event_json: String, - pub relays: Vec<String>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RadrootsAppRemoteSignerProgressUpdate { - AuthChallenge { url: String }, -} - -#[derive(Debug, Clone)] -pub enum RadrootsAppRemoteSignerPendingPollOutcome { - PendingApproval, - Approved(RadrootsAppRemoteSignerApprovedSession), - TransportFailure { message: String }, - Rejected { message: String }, - FatalError { message: String }, -} - -pub(crate) struct RadrootsAppRemoteSignerPendingPoller { - client: ConnectedRemoteSignerSessionClient, -} - -struct ConnectedRemoteSignerSessionClient { - runtime: tokio::runtime::Runtime, - client_identity: RadrootsIdentity, - target: RadrootsAppRemoteSignerTarget, - client: RadrootsNostrClient, - notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>, -} - -pub fn radroots_app_remote_signer_connect_pending( - input: &str, -) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { - let target = radroots_app_remote_signer_preview(input)?; - connect_pending_session(target) -} - -pub fn radroots_app_remote_signer_poll_pending_session( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, -) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> { - radroots_app_remote_signer_poll_pending_session_with_progress( - record, - client_secret_key_hex, - |_| {}, - ) -} - -pub fn radroots_app_remote_signer_poll_pending_session_with_progress<F>( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - mut progress: F, -) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - let mut poller = radroots_app_remote_signer_open_pending_poller(record, client_secret_key_hex)?; - radroots_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress) -} - -pub(crate) fn radroots_app_remote_signer_open_pending_poller( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, -) -> Result<RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerError> { - let client_identity = load_client_identity(client_secret_key_hex)?; - let target = target_for_record(record); - Ok(RadrootsAppRemoteSignerPendingPoller { - client: ConnectedRemoteSignerSessionClient::connect(client_identity, target)?, - }) -} - -pub(crate) fn radroots_app_remote_signer_poll_pending_poller_with_progress<F>( - poller: &mut RadrootsAppRemoteSignerPendingPoller, - progress: &mut F, -) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - poller.poll_with_progress(progress) -} - -pub fn radroots_app_remote_signer_sign_kind1_note( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - content: &str, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> { - radroots_app_remote_signer_sign_kind1_note_with_progress( - record, - client_secret_key_hex, - content, - |_| {}, - ) -} - -pub fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - content: &str, - mut progress: F, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - sign_kind1_note(record, client_secret_key_hex, content, &mut progress) -} - -pub fn radroots_app_remote_signer_sign_unsigned_event( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - unsigned_event: UnsignedEvent, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> { - radroots_app_remote_signer_sign_unsigned_event_with_progress( - record, - client_secret_key_hex, - unsigned_event, - |_| {}, - ) -} - -pub fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - unsigned_event: UnsignedEvent, - mut progress: F, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - sign_unsigned_event(record, client_secret_key_hex, unsigned_event, &mut progress) -} - -fn connect_pending_session( - target: RadrootsAppRemoteSignerTarget, -) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { - let client_identity = RadrootsIdentity::generate(); - let connect_request = connect_request_for_target(&target)?; - let response = execute_request( - &client_identity, - &target, - RadrootsNostrConnectMethod::Connect, - connect_request, - CONNECT_TIMEOUT, - )?; - - match response { - RadrootsNostrConnectResponse::ConnectAcknowledged - | RadrootsNostrConnectResponse::ConnectSecretEcho(_) => { - Ok(RadrootsAppRemoteSignerPendingSession { - record: RadrootsAppRemoteSignerSessionRecord::pending( - client_identity.to_public(), - target.signer_identity, - target.relays, - ), - client_secret_key_hex: client_identity.secret_key_hex(), - }) - } - other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { - method: RadrootsNostrConnectMethod::Connect, - response: format!("{other:?}"), - }), - } -} - -fn connect_request_for_target( - target: &RadrootsAppRemoteSignerTarget, -) -> Result<RadrootsNostrConnectRequest, RadrootsAppRemoteSignerError> { - Ok(RadrootsNostrConnectRequest::Connect { - remote_signer_public_key: parse_public_key_hex( - target.signer_identity.public_key_hex.as_str(), - )?, - secret: target.connect_secret.clone(), - requested_permissions: target.requested_permissions.clone(), - }) -} - -fn sign_kind1_note<F>( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - content: &str, - progress: &mut F, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - if !record.allows_sign_event_kind1() { - return Err(RadrootsAppRemoteSignerError::ConnectFailed( - "remote signer has not approved sign_event:kind:1".to_owned(), - )); - } - let user_identity = record.user_identity.as_ref().ok_or_else(|| { - RadrootsAppRemoteSignerError::ConnectFailed( - "remote signer session is missing the approved user identity".to_owned(), - ) - })?; - let unsigned_event = EventBuilder::text_note(content.trim()) - .build(parse_public_key_hex(user_identity.public_key_hex.as_str())?); - sign_unsigned_event(record, client_secret_key_hex, unsigned_event, progress) -} - -fn sign_unsigned_event<F>( - record: &RadrootsAppRemoteSignerSessionRecord, - client_secret_key_hex: &str, - unsigned_event: UnsignedEvent, - progress: &mut F, -) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> -where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), -{ - let client_identity = load_client_identity(client_secret_key_hex)?; - let target = target_for_record(record); - let mut client = ConnectedRemoteSignerSessionClient::connect(client_identity, target)?; - let relays = client.sync_relays_if_allowed(record, progress)?; - let response = client.execute_request_with_progress( - RadrootsNostrConnectMethod::SignEvent, - RadrootsNostrConnectRequest::SignEvent(unsigned_event), - SIGN_EVENT_TIMEOUT, - progress, - )?; - - match response { - RadrootsNostrConnectResponse::SignedEvent(event) => { - Ok(RadrootsAppRemoteSignerSignedEvent { - event_id_hex: event.id.to_hex(), - event_json: event.as_json(), - relays, - }) - } - RadrootsNostrConnectResponse::Error { error, .. } => { - Err(RadrootsAppRemoteSignerError::ConnectFailed(error)) - } - other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { - method: RadrootsNostrConnectMethod::SignEvent, - response: format!("{other:?}"), - }), - } -} - -fn execute_request( - client_identity: &RadrootsIdentity, - target: &RadrootsAppRemoteSignerTarget, - method: RadrootsNostrConnectMethod, - request: RadrootsNostrConnectRequest, - request_timeout: Duration, -) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> { - let mut client = - ConnectedRemoteSignerSessionClient::connect(client_identity.clone(), target.clone())?; - client.execute_request_with_progress(method, request, request_timeout, &mut |_| {}) -} - -impl RadrootsAppRemoteSignerPendingPoller { - fn poll_with_progress<F>( - &mut self, - progress: &mut F, - ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> - where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), - { - match self.client.execute_request_with_progress( - RadrootsNostrConnectMethod::GetSessionCapability, - RadrootsNostrConnectRequest::GetSessionCapability, - GET_SESSION_CAPABILITY_TIMEOUT, - progress, - ) { - Ok(response) => Ok(classify_pending_poll_response(response)), - Err(error) => Ok(classify_pending_poll_error(error)), - } - } -} - -impl ConnectedRemoteSignerSessionClient { - fn connect( - client_identity: RadrootsIdentity, - target: RadrootsAppRemoteSignerTarget, - ) -> Result<Self, RadrootsAppRemoteSignerError> { - let runtime = Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - let client = RadrootsNostrClient::from_identity(&client_identity); - let notifications = runtime.block_on(async { - for relay in &target.relays { - client.add_relay(relay).await.map_err(|error| { - RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()) - })?; - } - client.connect().await; - let filter = radroots_nostr_filter_tag( - RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) - .since(RadrootsNostrTimestamp::now()), - "p", - vec![client_identity.public_key_hex()], - ) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - let notifications = client.notifications(); - client - .subscribe(filter, None) - .await - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - Ok::<_, RadrootsAppRemoteSignerError>(notifications) - })?; - - Ok(Self { - runtime, - client_identity, - target, - client, - notifications, - }) - } - - fn sync_relays_if_allowed<F>( - &mut self, - record: &RadrootsAppRemoteSignerSessionRecord, - progress: &mut F, - ) -> Result<Vec<String>, RadrootsAppRemoteSignerError> - where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), - { - if !record.allows_switch_relays() { - return Ok(self.target.relays.clone()); - } - - match self.execute_request_with_progress( - RadrootsNostrConnectMethod::SwitchRelays, - RadrootsNostrConnectRequest::SwitchRelays, - SWITCH_RELAYS_TIMEOUT, - progress, - )? { - RadrootsNostrConnectResponse::RelayList(relays) => { - let relays: Vec<String> = - relays.into_iter().map(|relay| relay.to_string()).collect(); - self.target.relays = relays.clone(); - Ok(relays) - } - RadrootsNostrConnectResponse::RelayListUnchanged => Ok(self.target.relays.clone()), - RadrootsNostrConnectResponse::Error { error, .. } => { - Err(RadrootsAppRemoteSignerError::ConnectFailed(format!( - "remote signer rejected relay update: {error}" - ))) - } - other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { - method: RadrootsNostrConnectMethod::SwitchRelays, - response: format!("{other:?}"), - }), - } - } - - fn execute_request_with_progress<F>( - &mut self, - method: RadrootsNostrConnectMethod, - request: RadrootsNostrConnectRequest, - request_timeout: Duration, - progress: &mut F, - ) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> - where - F: FnMut(RadrootsAppRemoteSignerProgressUpdate), - { - let request_id = next_request_id(method.to_string().as_str()); - let response_method = method.clone(); - self.runtime.block_on(async { - let event_builder = build_request_event( - &self.client_identity, - &self.target.signer_identity, - request_id.as_str(), - request, - )?; - self.client - .send_event_builder(event_builder) - .await - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - - timeout(request_timeout, async { - loop { - let notification = match self.notifications.recv().await { - Ok(notification) => notification, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => { - return Err(RadrootsAppRemoteSignerError::ConnectFailed( - "remote signer notification stream closed".to_owned(), - )); - } - }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification - else { - continue; - }; - let event = *event; - if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) { - continue; - } - if event.pubkey.to_hex() != self.target.signer_identity.public_key_hex { - continue; - } - match parse_response_event( - &self.client_identity, - &event, - &response_method, - request_id.as_str(), - )? { - Some(RadrootsNostrConnectResponse::AuthUrl(url)) => { - progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url }); - } - Some(response) => return Ok(response), - None => continue, - } - } - }) - .await - .map_err(|_| RadrootsAppRemoteSignerError::RequestTimedOut { - method: response_method.clone(), - })? - }) - } -} - -fn build_request_event( - client_identity: &RadrootsIdentity, - signer_identity: &RadrootsIdentityPublic, - request_id: &str, - request: RadrootsNostrConnectRequest, -) -> Result<RadrootsNostrEventBuilder, RadrootsAppRemoteSignerError> { - let payload = serde_json::to_string(&RadrootsNostrConnectRequestMessage::new( - request_id.to_owned(), - request, - )) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - let signer_public_key = parse_public_key_hex(signer_identity.public_key_hex.as_str())?; - let ciphertext = nip44::encrypt( - client_identity.keys().secret_key(), - &signer_public_key, - payload, - Version::V2, - ) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - Ok(RadrootsNostrEventBuilder::new( - radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND), - ciphertext, - ) - .tags(vec![RadrootsNostrTag::public_key(signer_public_key)])) -} - -fn parse_response_event( - client_identity: &RadrootsIdentity, - event: &RadrootsNostrEvent, - method: &RadrootsNostrConnectMethod, - request_id: &str, -) -> Result<Option<RadrootsNostrConnectResponse>, RadrootsAppRemoteSignerError> { - let decrypted = nip44::decrypt( - client_identity.keys().secret_key(), - &event.pubkey, - &event.content, - ) - .map_err(|error| RadrootsAppRemoteSignerError::UnexpectedResponse { - method: method.clone(), - response: format!("failed to decrypt signer response: {error}"), - })?; - let envelope: RadrootsNostrConnectResponseEnvelope = - serde_json::from_str(&decrypted).map_err(|error| { - RadrootsAppRemoteSignerError::UnexpectedResponse { - method: method.clone(), - response: format!("failed to decode signer response envelope: {error}"), - } - })?; - if envelope.id != request_id { - return Ok(None); - } - let response = - RadrootsNostrConnectResponse::from_envelope(method, envelope).map_err(|error| { - RadrootsAppRemoteSignerError::UnexpectedResponse { - method: method.clone(), - response: format!("failed to decode signer response payload: {error}"), - } - })?; - Ok(Some(response)) -} - -fn classify_pending_poll_response( - response: RadrootsNostrConnectResponse, -) -> RadrootsAppRemoteSignerPendingPollOutcome { - match response.into_pending_connection_poll_outcome() { - RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key) => { - RadrootsAppRemoteSignerPendingPollOutcome::Approved( - RadrootsAppRemoteSignerApprovedSession { - user_identity: RadrootsIdentityPublic::new(public_key), - relays: Vec::new(), - approved_permissions: RadrootsNostrConnectPermissions::default(), - }, - ) - } - RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) => { - RadrootsAppRemoteSignerPendingPollOutcome::Approved( - RadrootsAppRemoteSignerApprovedSession { - user_identity: RadrootsIdentityPublic::new(capability.user_public_key), - relays: capability - .relays - .into_iter() - .map(|relay| relay.to_string()) - .collect(), - approved_permissions: capability.permissions, - }, - ) - } - RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval => { - RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval - } - RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message } => { - RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message } - } - RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url } => { - RadrootsAppRemoteSignerPendingPollOutcome::FatalError { - message: format!("unexpected remote signer authorization challenge: {url}"), - } - } - RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { response } => { - RadrootsAppRemoteSignerPendingPollOutcome::FatalError { - message: format!("unexpected remote signer response: {response}"), - } - } - } -} - -fn classify_pending_poll_error( - error: RadrootsAppRemoteSignerError, -) -> RadrootsAppRemoteSignerPendingPollOutcome { - match error { - RadrootsAppRemoteSignerError::RequestTimedOut { .. } => { - RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { - message: "remote signer did not respond yet".to_owned(), - } - } - RadrootsAppRemoteSignerError::ConnectFailed(message) => { - RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message } - } - RadrootsAppRemoteSignerError::UnexpectedResponse { .. } => { - RadrootsAppRemoteSignerPendingPollOutcome::FatalError { - message: error.to_string(), - } - } - other => RadrootsAppRemoteSignerPendingPollOutcome::FatalError { - message: other.to_string(), - }, - } -} - -fn next_request_id(prefix: &str) -> String { - let tick = REQUEST_COUNTER.fetch_add(1, Ordering::AcqRel); - format!("{prefix}-{tick}") -} - -fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsAppRemoteSignerError> { - nostr::PublicKey::parse(value) - .or_else(|_| nostr::PublicKey::from_hex(value)) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string())) -} - -fn load_client_identity( - client_secret_key_hex: &str, -) -> Result<RadrootsIdentity, RadrootsAppRemoteSignerError> { - RadrootsIdentity::from_secret_key_str(client_secret_key_hex) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string())) -} - -fn target_for_record( - record: &RadrootsAppRemoteSignerSessionRecord, -) -> RadrootsAppRemoteSignerTarget { - RadrootsAppRemoteSignerTarget { - source: crate::RadrootsAppRemoteSignerSource::BunkerUri, - signer_identity: record.signer_identity.clone(), - relays: record.relays.clone(), - connect_secret: None, - requested_permissions: if record.approved_permissions.is_empty() { - crate::radroots_app_remote_signer_requested_permissions() - } else { - record.approved_permissions.clone() - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::radroots_app_remote_signer_preview; - use nostr::PublicKey; - use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS, fixture_identity}; - use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectPermission, RadrootsNostrConnectRemoteSessionCapability, - }; - - fn fixture_public_key() -> PublicKey { - fixture_identity(&FIXTURE_ALICE) - .expect("identity") - .public_key() - } - - fn fixture_discovery_url() -> String { - format!( - "http://localhost/connect?uri={}", - url::form_urlencoded::byte_serialize( - format!("bunker://{}?relay={RELAY_PRIMARY_WSS}", FIXTURE_ALICE.npub).as_bytes() - ) - .collect::<String>() - ) - } - - #[test] - fn pending_connection_response_is_classified_as_pending_approval() { - let outcome = - classify_pending_poll_response(RadrootsNostrConnectResponse::PendingConnection); - - assert!(matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval - )); - } - - #[test] - fn signer_error_response_is_classified_as_rejected() { - let outcome = classify_pending_poll_response(RadrootsNostrConnectResponse::Error { - result: None, - error: "unauthorized".to_owned(), - }); - - assert!(matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message } - if message == "unauthorized" - )); - } - - #[test] - fn session_capability_success_is_classified_as_approved() { - let outcome = - classify_pending_poll_response(RadrootsNostrConnectResponse::RemoteSessionCapability( - RadrootsNostrConnectRemoteSessionCapability { - user_public_key: fixture_public_key(), - relays: vec![nostr::RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay")], - permissions: vec![ - RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "kind:1", - ), - RadrootsNostrConnectPermission::new( - RadrootsNostrConnectMethod::SwitchRelays, - ), - ] - .into(), - }, - )); - - assert!(matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::Approved( - RadrootsAppRemoteSignerApprovedSession { user_identity, approved_permissions, .. } - ) if user_identity.public_key_hex == fixture_public_key().to_hex() - && approved_permissions.to_string() == "sign_event:kind:1,switch_relays" - )); - } - - #[test] - fn timeout_error_is_classified_as_transport_failure() { - let outcome = classify_pending_poll_error(RadrootsAppRemoteSignerError::RequestTimedOut { - method: RadrootsNostrConnectMethod::GetSessionCapability, - }); - - assert!(matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message } - if message == "remote signer did not respond yet" - )); - } - - #[test] - fn unexpected_response_error_is_fatal() { - let outcome = - classify_pending_poll_error(RadrootsAppRemoteSignerError::UnexpectedResponse { - method: RadrootsNostrConnectMethod::GetSessionCapability, - response: "failed to decode signer response envelope: bad".to_owned(), - }); - - assert!(matches!( - outcome, - RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message } - if message.contains("unexpected `get_session_capability` response") - )); - } - - #[test] - fn connect_request_uses_explicit_requested_permissions() { - let target = - radroots_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview"); - - let request = connect_request_for_target(&target).expect("request"); - - match request { - RadrootsNostrConnectRequest::Connect { - requested_permissions, - .. - } => assert_eq!( - requested_permissions.to_string(), - "sign_event:kind:1,switch_relays" - ), - other => panic!("unexpected request: {other:?}"), - } - } - - #[test] - fn sign_kind1_note_output_carries_signed_relay_state() { - let signed_event = RadrootsAppRemoteSignerSignedEvent { - event_id_hex: "deadbeef".to_owned(), - event_json: "{\"id\":\"deadbeef\"}".to_owned(), - relays: vec!["ws://localhost:8080".to_owned()], - }; - - assert_eq!(signed_event.event_id_hex, "deadbeef"); - assert_eq!(signed_event.relays, vec!["ws://localhost:8080".to_owned()]); - } - - #[test] - fn target_for_record_uses_approved_permissions_when_available() { - let client_identity = fixture_identity(&FIXTURE_ALICE) - .expect("client") - .to_public(); - let signer_identity = fixture_identity(&FIXTURE_ALICE) - .expect("signer") - .to_public(); - let mut record = RadrootsAppRemoteSignerSessionRecord::pending( - client_identity, - signer_identity, - vec![RELAY_PRIMARY_WSS.to_owned()], - ); - record.approved_permissions = vec![RadrootsNostrConnectPermission::new( - RadrootsNostrConnectMethod::SwitchRelays, - )] - .into(); - - let target = target_for_record(&record); - - assert_eq!(target.requested_permissions.to_string(), "switch_relays"); - } -} diff --git a/crates/shared/remote_signer/src/session.rs b/crates/shared/remote_signer/src/session.rs @@ -1,501 +0,0 @@ -use crate::error::RadrootsAppRemoteSignerError; -use radroots_identity::RadrootsIdentityPublic; -use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, -}; -use serde::{Deserialize, Serialize}; -use std::io::Write; -use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub const RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION: u32 = 1; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum RadrootsAppRemoteSignerSessionStatus { - PendingApproval, - Active, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RadrootsAppRemoteSignerSessionRecord { - pub client_identity: RadrootsIdentityPublic, - pub signer_identity: RadrootsIdentityPublic, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub user_identity: Option<RadrootsIdentityPublic>, - pub relays: Vec<String>, - #[serde(default)] - pub approved_permissions: RadrootsNostrConnectPermissions, - pub status: RadrootsAppRemoteSignerSessionStatus, - pub created_at_unix: u64, - pub updated_at_unix: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RadrootsAppRemoteSignerSessionStoreState { - pub version: u32, - pub sessions: Vec<RadrootsAppRemoteSignerSessionRecord>, -} - -#[derive(Debug, Clone)] -pub struct RadrootsAppRemoteSignerSessionStoreLoadResult { - pub state: RadrootsAppRemoteSignerSessionStoreState, - pub recovered_from_corruption: bool, -} - -impl Default for RadrootsAppRemoteSignerSessionStoreState { - fn default() -> Self { - Self { - version: RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION, - sessions: Vec::new(), - } - } -} - -impl RadrootsAppRemoteSignerSessionRecord { - pub fn pending( - client_identity: RadrootsIdentityPublic, - signer_identity: RadrootsIdentityPublic, - relays: Vec<String>, - ) -> Self { - let now = now_unix_secs(); - Self { - client_identity, - signer_identity, - user_identity: None, - relays, - approved_permissions: RadrootsNostrConnectPermissions::default(), - status: RadrootsAppRemoteSignerSessionStatus::PendingApproval, - created_at_unix: now, - updated_at_unix: now, - } - } - - pub fn account_id(&self) -> Option<&str> { - self.user_identity - .as_ref() - .map(|identity| identity.id.as_str()) - } - - pub fn client_account_id(&self) -> &str { - self.client_identity.id.as_str() - } - - pub fn approved_permission_labels(&self) -> Vec<String> { - self.approved_permissions - .as_slice() - .iter() - .map(ToString::to_string) - .collect() - } - - pub fn allows_sign_event_kind1(&self) -> bool { - self.approved_permissions - .as_slice() - .iter() - .any(|permission| { - permission_matches( - permission, - &RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "kind:1", - ), - ) - }) - } - - pub fn allows_switch_relays(&self) -> bool { - self.approved_permissions - .as_slice() - .iter() - .any(|permission| { - permission_matches( - permission, - &RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), - ) - }) - } -} - -impl RadrootsAppRemoteSignerSessionStoreState { - pub fn load(path: &Path) -> Result<Self, RadrootsAppRemoteSignerError> { - Ok(Self::load_with_recovery(path)?.state) - } - - pub fn load_with_recovery( - path: &Path, - ) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerError> { - match std::fs::read(path) { - Ok(contents) => Self::load_bytes(path, contents), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - Ok(RadrootsAppRemoteSignerSessionStoreLoadResult { - state: Self::default(), - recovered_from_corruption: false, - }) - } - Err(error) => Err(RadrootsAppRemoteSignerError::SessionStoreIo( - error.to_string(), - )), - } - } - - pub fn save(&self, path: &Path) -> Result<(), RadrootsAppRemoteSignerError> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?; - } - let json = serde_json::to_string_pretty(self) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?; - let temp_path = temporary_store_path(path); - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(temp_path.as_path()) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?; - if let Err(error) = (|| -> Result<(), std::io::Error> { - file.write_all(json.as_bytes())?; - file.flush()?; - file.sync_all() - })() { - let _ = std::fs::remove_file(temp_path.as_path()); - return Err(RadrootsAppRemoteSignerError::SessionStoreIo( - error.to_string(), - )); - } - - #[cfg(windows)] - if path.exists() { - std::fs::remove_file(path) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string()))?; - } - - std::fs::rename(temp_path.as_path(), path) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string())) - } - - pub fn pending_session(&self) -> Option<&RadrootsAppRemoteSignerSessionRecord> { - self.sessions - .iter() - .find(|record| record.status == RadrootsAppRemoteSignerSessionStatus::PendingApproval) - } - - pub fn active_session_for_account_id( - &self, - account_id: &str, - ) -> Option<&RadrootsAppRemoteSignerSessionRecord> { - self.sessions.iter().find(|record| { - record.status == RadrootsAppRemoteSignerSessionStatus::Active - && record.account_id() == Some(account_id) - }) - } - - pub fn upsert_pending( - &mut self, - pending: RadrootsAppRemoteSignerSessionRecord, - ) -> Result<(), RadrootsAppRemoteSignerError> { - if self.pending_session().is_some() { - return Err(RadrootsAppRemoteSignerError::PendingSessionExists); - } - self.sessions - .retain(|record| record.client_account_id() != pending.client_account_id()); - self.sessions.push(pending); - Ok(()) - } - - pub fn activate_session( - &mut self, - client_account_id: &str, - user_identity: RadrootsIdentityPublic, - relays: Vec<String>, - approved_permissions: RadrootsNostrConnectPermissions, - ) -> Option<RadrootsAppRemoteSignerSessionRecord> { - let now = now_unix_secs(); - self.sessions.retain(|record| { - !(record.status == RadrootsAppRemoteSignerSessionStatus::Active - && record.account_id() == Some(user_identity.id.as_str())) - }); - let record = self - .sessions - .iter_mut() - .find(|record| record.client_account_id() == client_account_id)?; - record.user_identity = Some(user_identity); - record.relays = relays; - record.approved_permissions = approved_permissions; - record.status = RadrootsAppRemoteSignerSessionStatus::Active; - record.updated_at_unix = now; - Some(record.clone()) - } - - pub fn remove_pending_session(&mut self) -> Option<RadrootsAppRemoteSignerSessionRecord> { - let index = self.sessions.iter().position(|record| { - record.status == RadrootsAppRemoteSignerSessionStatus::PendingApproval - })?; - Some(self.sessions.remove(index)) - } - - pub fn remove_active_session_for_account_id( - &mut self, - account_id: &str, - ) -> Option<RadrootsAppRemoteSignerSessionRecord> { - let index = self.sessions.iter().position(|record| { - record.status == RadrootsAppRemoteSignerSessionStatus::Active - && record.account_id() == Some(account_id) - })?; - Some(self.sessions.remove(index)) - } - - fn load_bytes( - path: &Path, - contents: Vec<u8>, - ) -> Result<RadrootsAppRemoteSignerSessionStoreLoadResult, RadrootsAppRemoteSignerError> { - let contents = String::from_utf8(contents).map_err(|error| { - RadrootsAppRemoteSignerError::InvalidSessionStore(format!( - "session store was not valid utf-8: {error}" - )) - }); - - let contents = match contents { - Ok(contents) => contents, - Err(error) => { - quarantine_invalid_store(path)?; - let _ = error; - return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult { - state: Self::default(), - recovered_from_corruption: true, - }); - } - }; - - let state = match serde_json::from_str::<Self>(&contents) { - Ok(state) => state, - Err(error) => { - quarantine_invalid_store(path)?; - let _ = error; - return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult { - state: Self::default(), - recovered_from_corruption: true, - }); - } - }; - - if state.version != RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION { - quarantine_invalid_store(path)?; - return Ok(RadrootsAppRemoteSignerSessionStoreLoadResult { - state: Self::default(), - recovered_from_corruption: true, - }); - } - - Ok(RadrootsAppRemoteSignerSessionStoreLoadResult { - state, - recovered_from_corruption: false, - }) - } -} - -fn permission_matches( - granted_permission: &RadrootsNostrConnectPermission, - required_permission: &RadrootsNostrConnectPermission, -) -> bool { - if granted_permission.method != required_permission.method { - return false; - } - - match ( - &granted_permission.method, - granted_permission.parameter.as_deref(), - required_permission.parameter.as_deref(), - ) { - (RadrootsNostrConnectMethod::SignEvent, None, _) => true, - (RadrootsNostrConnectMethod::SignEvent, Some(parameter), Some(required)) => { - parameter == required || parameter == sign_event_kind_suffix(required) - } - (_, None, _) => true, - (_, Some(parameter), Some(required)) => parameter == required, - (_, Some(_), None) => false, - } -} - -fn sign_event_kind_suffix(value: &str) -> &str { - value.strip_prefix("kind:").unwrap_or(value) -} - -fn now_unix_secs() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs()) - .unwrap_or(0) -} - -fn temporary_store_path(path: &Path) -> std::path::PathBuf { - let process_id = std::process::id(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - path.with_extension(format!("json.tmp-{process_id}-{timestamp}")) -} - -fn quarantine_invalid_store(path: &Path) -> Result<(), RadrootsAppRemoteSignerError> { - let process_id = std::process::id(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs()) - .unwrap_or(0); - let file_name = path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("remote-signer-sessions.json"); - let quarantine_path = - path.with_file_name(format!("{file_name}.corrupt-{timestamp}-{process_id}")); - std::fs::rename(path, quarantine_path.as_path()) - .map_err(|error| RadrootsAppRemoteSignerError::SessionStoreIo(error.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use radroots_app_test_support::{FIXTURE_ALICE, FIXTURE_BOB, fixture_identity}; - use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, - }; - - fn fixture_public( - label: &radroots_app_test_support::RadrootsAppApprovedFixtureIdentity, - ) -> RadrootsIdentityPublic { - fixture_identity(label).expect("identity").to_public() - } - - fn pending_record() -> RadrootsAppRemoteSignerSessionRecord { - RadrootsAppRemoteSignerSessionRecord::pending( - fixture_public(&FIXTURE_ALICE), - fixture_public(&FIXTURE_BOB), - vec!["wss://relay.example.com".to_owned()], - ) - } - - #[test] - fn pending_store_round_trips() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - let mut state = RadrootsAppRemoteSignerSessionStoreState::default(); - state.upsert_pending(pending_record()).expect("pending"); - state.save(path.as_path()).expect("save"); - - let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load"); - - assert_eq!(loaded.sessions.len(), 1); - assert_eq!( - loaded.sessions[0].status, - RadrootsAppRemoteSignerSessionStatus::PendingApproval - ); - } - - #[test] - fn activate_session_replaces_pending_with_active_user_identity() { - let mut state = RadrootsAppRemoteSignerSessionStoreState::default(); - let pending = pending_record(); - let client_account_id = pending.client_account_id().to_owned(); - state.upsert_pending(pending).expect("pending"); - - let alice_public = fixture_public(&FIXTURE_ALICE); - let active = state - .activate_session( - client_account_id.as_str(), - alice_public.clone(), - vec!["wss://relay.updated.example".to_owned()], - vec![ - RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "kind:1", - ), - RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), - ] - .into(), - ) - .expect("active"); - - assert_eq!(active.status, RadrootsAppRemoteSignerSessionStatus::Active); - assert_eq!(active.account_id(), Some(alice_public.id.as_str())); - assert_eq!( - active.relays, - vec!["wss://relay.updated.example".to_owned()] - ); - assert_eq!( - active.approved_permission_labels(), - vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()] - ); - assert!(active.allows_sign_event_kind1()); - assert!(active.allows_switch_relays()); - assert!(state.pending_session().is_none()); - } - - #[test] - fn remove_active_session_matches_user_account_id() { - let mut state = RadrootsAppRemoteSignerSessionStoreState::default(); - let pending = pending_record(); - let client_account_id = pending.client_account_id().to_owned(); - state.upsert_pending(pending).expect("pending"); - let alice_public = fixture_public(&FIXTURE_ALICE); - state.activate_session( - client_account_id.as_str(), - alice_public.clone(), - vec!["wss://relay.updated.example".to_owned()], - RadrootsNostrConnectPermissions::default(), - ); - - let removed = state - .remove_active_session_for_account_id(alice_public.id.as_str()) - .expect("removed"); - - assert_eq!(removed.account_id(), Some(alice_public.id.as_str())); - assert!(state.sessions.is_empty()); - } - - #[test] - fn load_recovers_from_invalid_json_by_quarantining_store() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - std::fs::write(path.as_path(), "{invalid").expect("write invalid"); - - let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load"); - - assert!(loaded.sessions.is_empty()); - assert!(!path.exists()); - let quarantined = std::fs::read_dir(temp.path()) - .expect("read dir") - .filter_map(|entry| entry.ok()) - .any(|entry| entry.file_name().to_string_lossy().contains("corrupt")); - assert!(quarantined); - } - - #[test] - fn load_recovers_from_unsupported_schema_version() { - let temp = tempfile::tempdir().expect("tempdir"); - let path = temp.path().join("sessions.json"); - std::fs::write(path.as_path(), r#"{"version":999,"sessions":[]}"#).expect("write invalid"); - - let loaded = RadrootsAppRemoteSignerSessionStoreState::load(path.as_path()).expect("load"); - - assert_eq!( - loaded.version, - RADROOTS_APP_REMOTE_SIGNER_SESSION_STORE_VERSION - ); - assert!(loaded.sessions.is_empty()); - assert!(!path.exists()); - } - - #[test] - fn active_session_permission_helpers_respect_sign_event_and_switch_relays() { - let mut record = pending_record(); - record.user_identity = Some(fixture_public(&FIXTURE_ALICE)); - record.status = RadrootsAppRemoteSignerSessionStatus::Active; - record.approved_permissions = vec![RadrootsNostrConnectPermission::with_parameter( - RadrootsNostrConnectMethod::SignEvent, - "1", - )] - .into(); - - assert!(record.allows_sign_event_kind1()); - assert!(!record.allows_switch_relays()); - } -} diff --git a/crates/shared/test_support/Cargo.toml b/crates/shared/test_support/Cargo.toml @@ -1,17 +0,0 @@ -[package] -name = "radroots_app_test_support" -authors.workspace = true -version.workspace = true -edition.workspace = true -license.workspace = true -rust-version.workspace = true -repository.workspace = true -homepage.workspace = true -description = "Rad Roots app test support" -publish = false - -[lints] -workspace = true - -[dependencies] -radroots_identity.workspace = true diff --git a/crates/shared/test_support/src/lib.rs b/crates/shared/test_support/src/lib.rs @@ -1,100 +0,0 @@ -#![forbid(unsafe_code)] - -use radroots_identity::{ - RadrootsIdentity, RadrootsIdentityEncryptedSecretKeyOptions, - RadrootsIdentityEncryptedSecretKeySecurity, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RadrootsAppApprovedFixtureIdentity { - pub label: &'static str, - pub account_id: &'static str, - pub username: &'static str, - pub email: &'static str, - pub secret_key_hex: &'static str, - pub nsec: &'static str, - pub npub: &'static str, -} - -pub const FIXTURE_ALICE: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity { - label: "fixture_alice", - account_id: "fixture-account-alice", - username: "fixture_alice", - email: "fixture_alice@fixtures.test", - secret_key_hex: "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5", - nsec: "nsec1zrznqntvnt36rgt00ps0rny0tca8vgj6ye3m82vf5rthtyvm0h6syu7drz", - npub: "npub1tp2ez55a5zatxxemrv0eses3ea05xhw2snuh3jy7azjqejn3q00s3vy5a9", -}; - -pub const FIXTURE_BOB: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity { - label: "fixture_bob", - account_id: "fixture-account-bob", - username: "fixture_bob", - email: "fixture_bob@fixtures.test", - secret_key_hex: "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8", - nsec: "nsec1tyujayrg7ejrrvf0wqscldsjs89kksea0unu2htp7xnrlcdfdluqrjya9h", - npub: "npub1uqnxu08mp55gd7guw06ls68nhxp8xuf7tlxe0sypvcl42x9ykwhsd55k2g", -}; - -pub const FIXTURE_CAROL: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity { - label: "fixture_carol", - account_id: "fixture-account-carol", - username: "fixture_carol", - email: "fixture_carol@fixtures.test", - secret_key_hex: "4d6c20fdd86857de77ff5cfa5c545751ba2efd126e0b6642dae9764d782d6509", - nsec: "nsec1f4kzplwcdptaualltna9c4zh2xazalgjdc9kvsk6a9my67pdv5ys2pqkaj", - npub: "npub1r9ft33558zvtemluludhdxwy5a66f5fmf2d6qztt5fh0q3yjhvwqgzmkl6", -}; - -pub const FIXTURE_DIEGO: RadrootsAppApprovedFixtureIdentity = RadrootsAppApprovedFixtureIdentity { - label: "fixture_diego", - account_id: "fixture-account-diego", - username: "fixture_diego", - email: "fixture_diego@fixtures.test", - secret_key_hex: "9de56c1fdfce9ab00af85b3d7003c1d15cffb84cdf303c3a83c1a3fb1a2d0db0", - nsec: "nsec1nhjkc87le6dtqzhctv7hqq7p69w0lwzvmucrcw5rcx3lkx3dpkcqkrmgp5", - npub: "npub1t5l2kmncadlyv757r94xx3tvn7hmj0ac3dc99wpj9xrs3zvj82jqwwcglm", -}; - -pub const RELAY_PRIMARY_WSS: &str = "wss://relay.example.com"; -pub const RELAY_SECONDARY_WSS: &str = "wss://relay-2.example.com"; -pub const RELAY_TERTIARY_WSS: &str = "wss://relay-3.example.com"; - -pub const APP_PRIMARY_URL: &str = "https://app.example.com"; -pub const API_PRIMARY_URL: &str = "https://api.example.com"; -pub const CDN_PRIMARY_URL: &str = "https://cdn.example.com"; -pub const FIXTURE_BACKUP_PASSWORD: &str = "fixture-backup-password"; - -pub fn fixture_identity( - fixture: &RadrootsAppApprovedFixtureIdentity, -) -> Result<RadrootsIdentity, radroots_identity::IdentityError> { - RadrootsIdentity::from_secret_key_str(fixture.secret_key_hex) -} - -pub fn fixture_identity_ncryptsec( - fixture: &RadrootsAppApprovedFixtureIdentity, - password: &str, -) -> Result<String, radroots_identity::IdentityError> { - fixture_identity(fixture)?.encrypt_secret_key_ncryptsec_with_options( - password, - RadrootsIdentityEncryptedSecretKeyOptions { - log_n: 10, - key_security: RadrootsIdentityEncryptedSecretKeySecurity::Weak, - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn approved_fixture_identities_match_exported_strings() { - for fixture in [FIXTURE_ALICE, FIXTURE_BOB, FIXTURE_CAROL, FIXTURE_DIEGO] { - let identity = fixture_identity(&fixture).expect("fixture identity"); - assert_eq!(identity.secret_key_hex(), fixture.secret_key_hex); - assert_eq!(identity.nsec(), fixture.nsec); - assert_eq!(identity.npub(), fixture.npub); - } - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/build.gradle.kts b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/build.gradle.kts @@ -1,32 +0,0 @@ -plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") -} - -android { - namespace = "org.radroots.app.android.security" - compileSdk = 34 - - defaultConfig { - minSdk = 26 - consumerProguardFiles("consumer-rules.pro") - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - testOptions { - unitTests.isIncludeAndroidResources = false - } -} - -dependencies { - implementation("androidx.biometric:biometric:1.1.0") - testImplementation("junit:junit:4.13.2") -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/consumer-rules.pro b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/consumer-rules.pro @@ -1 +0,0 @@ - diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/settings.gradle.kts b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/settings.gradle.kts @@ -1,17 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(org.gradle.api.initialization.resolve.RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "RadRootsAndroidSecurity" diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/AndroidManifest.xml b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/AndroidManifest.xml @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android" /> diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeySecurityLevel.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeySecurityLevel.kt @@ -1,73 +0,0 @@ -package org.radroots.app.android.security - -import android.os.Build -import android.security.keystore.KeyInfo -import android.security.keystore.KeyProperties - -internal enum class RadRootsAndroidKeySecurityLevel { - STRONGBOX, - TRUSTED_ENVIRONMENT, - SOFTWARE_OR_UNKNOWN, -} - -internal object RadRootsAndroidKeySecurityLevels { - fun fromKeyInfo(keyInfo: KeyInfo): RadRootsAndroidKeySecurityLevel { - return fromPlatformValues( - sdkInt = Build.VERSION.SDK_INT, - securityLevel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - keyInfo.securityLevel - } else { - null - }, - isInsideSecureHardware = isInsideSecureHardwareFallback(keyInfo), - ) - } - - fun fromPlatformValues( - sdkInt: Int, - securityLevel: Int?, - isInsideSecureHardware: Boolean, - ): RadRootsAndroidKeySecurityLevel { - if (sdkInt >= Build.VERSION_CODES.S && securityLevel != null) { - return when (securityLevel) { - KeyProperties.SECURITY_LEVEL_STRONGBOX -> RadRootsAndroidKeySecurityLevel.STRONGBOX - KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT, - KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE, - -> RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT - else -> RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN - } - } - - return if (isInsideSecureHardware) { - RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT - } else { - RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN - } - } - - @Suppress("DEPRECATION") - private fun isInsideSecureHardwareFallback(keyInfo: KeyInfo): Boolean { - return keyInfo.isInsideSecureHardware - } -} - -internal fun shouldRequestStrongBox( - policy: RadRootsAndroidSecretAccessPolicy, - sdkInt: Int, - hasStrongBoxFeature: Boolean, -): Boolean { - return policy.preferStrongBox && - sdkInt >= Build.VERSION_CODES.P && - hasStrongBoxFeature -} - -internal fun acceptsStrongBoxVerificationResult( - sdkInt: Int, - securityLevel: RadRootsAndroidKeySecurityLevel, -): Boolean { - return when (securityLevel) { - RadRootsAndroidKeySecurityLevel.STRONGBOX -> true - RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT -> sdkInt < Build.VERSION_CODES.S - RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN -> false - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeystoreSecretStore.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidKeystoreSecretStore.kt @@ -1,371 +0,0 @@ -package org.radroots.app.android.security - -import android.content.Context -import android.content.pm.PackageManager -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyInfo -import android.security.keystore.KeyProperties -import android.security.keystore.StrongBoxUnavailableException -import java.io.File -import java.nio.ByteBuffer -import java.nio.file.AtomicMoveNotSupportedException -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.GCMParameterSpec - -class RadRootsAndroidKeystoreSecretStore( - private val context: Context, -) { - fun putSecret( - servicePrefix: String, - namespace: String, - name: String, - value: ByteArray, - policy: RadRootsAndroidSecretAccessPolicy, - ) { - validateIdentifiers(servicePrefix, namespace, name) - requireSupportedPolicy(policy) - val key = getOrCreateKey(masterKeyAlias(servicePrefix, namespace), policy) - val cipher = Cipher.getInstance(cipherTransformation) - cipher.init(Cipher.ENCRYPT_MODE, key) - val iv = cipher.iv - val ciphertext = cipher.doFinal(value) - val target = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name) - writeSecretFile(target, encodeSecretBlob(iv, ciphertext)) - } - - fun getSecret( - servicePrefix: String, - namespace: String, - name: String, - ): ByteArray? { - validateIdentifiers(servicePrefix, namespace, name) - val target = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name) - val legacyTarget = RadRootsAndroidStoragePaths.legacySecretFile( - context.noBackupFilesDir, - servicePrefix, - namespace, - name, - ) - val source = when { - target.exists() -> target - legacyTarget.exists() -> legacyTarget - else -> null - } - if (source == null) { - return null - } - val secretBlob = readSecretFile(source) - val (iv, ciphertext) = decodeSecretBlob(secretBlob) - val cipher = Cipher.getInstance(cipherTransformation) - cipher.init( - Cipher.DECRYPT_MODE, - getOrCreateKey( - masterKeyAlias(servicePrefix, namespace), - RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET, - ), - GCMParameterSpec(gcmTagBits, iv), - ) - return try { - cipher.doFinal(ciphertext) - } catch (cause: Throwable) { - throw RadRootsAndroidSecurityError.KeystoreFailure( - "failed to decrypt secret", - cause, - ) - } - } - - fun deleteSecret( - servicePrefix: String, - namespace: String, - name: String, - ) { - validateIdentifiers(servicePrefix, namespace, name) - val current = RadRootsAndroidStoragePaths.secretFile(context, servicePrefix, namespace, name) - val legacy = RadRootsAndroidStoragePaths.legacySecretFile( - context.noBackupFilesDir, - servicePrefix, - namespace, - name, - ) - deleteSecretFileIfPresent(current) - deleteSecretFileIfPresent(legacy) - } - - fun deleteNamespace( - servicePrefix: String, - namespace: String, - ) { - validateNamespace(servicePrefix, namespace) - val secretsDir = RadRootsAndroidStoragePaths.secretsDir(context) - val prefix = RadRootsAndroidStoragePaths.namespaceFilePrefix(servicePrefix, namespace) - val children = secretsDir.listFiles().orEmpty() - for (child in children) { - if (!child.isFile || !child.name.startsWith(prefix) || !child.name.endsWith(".bin")) { - continue - } - if (!child.delete()) { - throw RadRootsAndroidSecurityError.StorageFailure( - "failed to delete encrypted secret namespace file", - ) - } - } - deleteKey(masterKeyAlias(servicePrefix, namespace)) - } - - fun resolveRadrootsBaseRoot(): File = RadRootsAndroidStoragePaths.baseRoot(context) - - private fun validateIdentifiers(servicePrefix: String, namespace: String, name: String) { - if (servicePrefix.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("service prefix must not be blank") - } - if (namespace.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("namespace must not be blank") - } - if (name.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("name must not be blank") - } - } - - private fun validateNamespace(servicePrefix: String, namespace: String) { - if (servicePrefix.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("service prefix must not be blank") - } - if (namespace.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("namespace must not be blank") - } - } - - private fun requireSupportedPolicy(policy: RadRootsAndroidSecretAccessPolicy) { - if (!policy.deviceLocalOnly) { - throw RadRootsAndroidSecurityError.InvalidInput( - "android security store supports only device-local secrets", - ) - } - } - - private fun masterKeyAlias(servicePrefix: String, namespace: String): String = - "org.radroots.app.android.security.v1.${RadRootsAndroidStoragePaths.secretFileId(servicePrefix, namespace, "master")}" - - private fun getOrCreateKey( - alias: String, - policy: RadRootsAndroidSecretAccessPolicy, - ): SecretKey { - val keyStore = KeyStore.getInstance(androidKeystoreProvider).apply { load(null) } - val existing = keyStore.getKey(alias, null) - if (existing is SecretKey) { - return existing - } - return createKey(alias, policy) - } - - private fun createKey( - alias: String, - policy: RadRootsAndroidSecretAccessPolicy, - ): SecretKey { - val requestStrongBox = shouldRequestStrongBox( - policy = policy, - sdkInt = Build.VERSION.SDK_INT, - hasStrongBoxFeature = canRequestStrongBox(), - ) - - return try { - val generated = generateKey(alias, policy, requestStrongBox = requestStrongBox) - if (requestStrongBox && !isAcceptableStrongBoxResult(generated.securityLevel)) { - deleteKey(alias) - return generateKey(alias, policy, requestStrongBox = false).key - } - generated.key - } catch (cause: StrongBoxUnavailableException) { - if (!requestStrongBox) { - throw keystoreFailure(cause) - } - deleteKey(alias) - generateKey(alias, policy, requestStrongBox = false).key - } catch (cause: Throwable) { - throw keystoreFailure(cause) - } - } - - private fun generateKey( - alias: String, - policy: RadRootsAndroidSecretAccessPolicy, - requestStrongBox: Boolean, - ): AndroidKeyCreationResult { - val builder = KeyGenParameterSpec.Builder( - alias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, - ) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setKeySize(256) - .setRandomizedEncryptionRequired(true) - - if (policy.userPresenceRequired) { - builder.setUserAuthenticationRequired(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - builder.setUserAuthenticationParameters( - 0, - KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL, - ) - } - } - - if (requestStrongBox && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - builder.setIsStrongBoxBacked(true) - } - - val keyGenerator = KeyGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_AES, - androidKeystoreProvider, - ) - keyGenerator.init(builder.build()) - val key = keyGenerator.generateKey() - return AndroidKeyCreationResult( - key = key, - securityLevel = resolveKeySecurityLevel(key), - ) - } - - private fun writeSecretFile(target: File, encoded: ByteArray) { - val parent = target.parentFile - ?: throw RadRootsAndroidSecurityError.StorageFailure("secret file has no parent directory") - if (!parent.exists() && !parent.mkdirs()) { - throw RadRootsAndroidSecurityError.StorageFailure("failed to create secret directory") - } - val temp = File(parent, "${target.name}.tmp") - try { - temp.writeBytes(encoded) - try { - Files.move( - temp.toPath(), - target.toPath(), - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING, - ) - } catch (_: AtomicMoveNotSupportedException) { - Files.move( - temp.toPath(), - target.toPath(), - StandardCopyOption.REPLACE_EXISTING, - ) - } - } catch (cause: Throwable) { - temp.delete() - throw RadRootsAndroidSecurityError.StorageFailure( - "failed to write encrypted secret file", - cause, - ) - } - } - - private fun readSecretFile(target: File): ByteArray { - return try { - target.readBytes() - } catch (cause: Throwable) { - throw RadRootsAndroidSecurityError.StorageFailure( - "failed to read encrypted secret file", - cause, - ) - } - } - - private fun encodeSecretBlob(iv: ByteArray, ciphertext: ByteArray): ByteArray { - val buffer = ByteBuffer.allocate(1 + Int.SIZE_BYTES + iv.size + ciphertext.size) - buffer.put(secretBlobVersion) - buffer.putInt(iv.size) - buffer.put(iv) - buffer.put(ciphertext) - return buffer.array() - } - - private fun decodeSecretBlob(blob: ByteArray): Pair<ByteArray, ByteArray> { - try { - val buffer = ByteBuffer.wrap(blob) - val version = buffer.get() - if (version != secretBlobVersion) { - throw RadRootsAndroidSecurityError.StorageFailure("unsupported encrypted secret version") - } - val ivLength = buffer.int - if (ivLength <= 0 || ivLength > buffer.remaining()) { - throw RadRootsAndroidSecurityError.StorageFailure("invalid encrypted secret iv length") - } - val iv = ByteArray(ivLength) - buffer.get(iv) - val ciphertext = ByteArray(buffer.remaining()) - buffer.get(ciphertext) - return iv to ciphertext - } catch (error: RadRootsAndroidSecurityError.StorageFailure) { - throw error - } catch (cause: Throwable) { - throw RadRootsAndroidSecurityError.StorageFailure( - "failed to decode encrypted secret file", - cause, - ) - } - } - - private fun canRequestStrongBox(): Boolean { - return context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) - } - - private fun isAcceptableStrongBoxResult( - securityLevel: RadRootsAndroidKeySecurityLevel, - ): Boolean { - return acceptsStrongBoxVerificationResult( - sdkInt = Build.VERSION.SDK_INT, - securityLevel = securityLevel, - ) - } - - private fun resolveKeySecurityLevel(key: SecretKey): RadRootsAndroidKeySecurityLevel { - val keyFactory = SecretKeyFactory.getInstance(key.algorithm, androidKeystoreProvider) - val keyInfo = keyFactory.getKeySpec(key, KeyInfo::class.java) as KeyInfo - return RadRootsAndroidKeySecurityLevels.fromKeyInfo(keyInfo) - } - - private fun deleteKey(alias: String) { - val keyStore = KeyStore.getInstance(androidKeystoreProvider).apply { load(null) } - if (keyStore.containsAlias(alias)) { - keyStore.deleteEntry(alias) - } - } - - private fun deleteSecretFileIfPresent(target: File) { - if (!target.exists()) { - return - } - if (!target.delete()) { - throw RadRootsAndroidSecurityError.StorageFailure("failed to delete encrypted secret file") - } - } - - private fun keystoreFailure(cause: Throwable): RadRootsAndroidSecurityError.KeystoreFailure { - return when (cause) { - is RadRootsAndroidSecurityError.KeystoreFailure -> cause - else -> RadRootsAndroidSecurityError.KeystoreFailure( - "failed to create keystore secret key", - cause, - ) - } - } - - private companion object { - const val androidKeystoreProvider = "AndroidKeyStore" - const val cipherTransformation = "AES/GCM/NoPadding" - const val gcmTagBits = 128 - const val secretBlobVersion: Byte = 1 - } -} - -private data class AndroidKeyCreationResult( - val key: SecretKey, - val securityLevel: RadRootsAndroidKeySecurityLevel, -) diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecretAccessPolicy.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecretAccessPolicy.kt @@ -1,15 +0,0 @@ -package org.radroots.app.android.security - -data class RadRootsAndroidSecretAccessPolicy( - val deviceLocalOnly: Boolean, - val userPresenceRequired: Boolean, - val preferStrongBox: Boolean, -) { - companion object { - val SECURE_LOCAL_SECRET = RadRootsAndroidSecretAccessPolicy( - deviceLocalOnly = true, - userPresenceRequired = false, - preferStrongBox = true, - ) - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityBridge.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityBridge.kt @@ -1,197 +0,0 @@ -package org.radroots.app.android.security - -import android.content.Context -import androidx.fragment.app.FragmentActivity - -object RadRootsAndroidSecurityBridge { - const val STATUS_SUCCESS = 0 - const val STATUS_NOT_FOUND = 1 - const val STATUS_INVALID_INPUT = 2 - const val STATUS_ERROR = 3 - - const val USER_PRESENCE_RESULT_NONE = 0 - const val USER_PRESENCE_RESULT_SUCCESS = 1 - const val USER_PRESENCE_RESULT_ERROR = 2 - - @Volatile - private var applicationContext: Context? = null - - @Volatile - private var currentActivity: FragmentActivity? = null - - @Volatile - private var lastErrorMessage: String? = null - - @Volatile - private var userPresenceVerificationPending: Boolean = false - - @Volatile - private var userPresenceVerificationResult: Int = USER_PRESENCE_RESULT_NONE - - @JvmStatic - fun initialize(context: Context) { - applicationContext = context.applicationContext - currentActivity = context as? FragmentActivity - clearError() - } - - @JvmStatic - fun putSecret( - servicePrefix: String, - namespace: String, - name: String, - value: ByteArray, - deviceLocalOnly: Boolean, - userPresenceRequired: Boolean, - preferStrongBox: Boolean, - ): Int { - return try { - secretStore().putSecret( - servicePrefix = servicePrefix, - namespace = namespace, - name = name, - value = value, - policy = RadRootsAndroidSecretAccessPolicy( - deviceLocalOnly = deviceLocalOnly, - userPresenceRequired = userPresenceRequired, - preferStrongBox = preferStrongBox, - ), - ) - clearError() - STATUS_SUCCESS - } catch (cause: Throwable) { - captureError(cause) - } - } - - @JvmStatic - fun getSecret( - servicePrefix: String, - namespace: String, - name: String, - ): ByteArray? { - return try { - val secret = secretStore().getSecret(servicePrefix, namespace, name) - clearError() - secret - } catch (cause: Throwable) { - captureError(cause) - null - } - } - - @JvmStatic - fun deleteSecret( - servicePrefix: String, - namespace: String, - name: String, - ): Int { - return try { - secretStore().deleteSecret(servicePrefix, namespace, name) - clearError() - STATUS_SUCCESS - } catch (cause: Throwable) { - captureError(cause) - } - } - - @JvmStatic - fun deleteSecretNamespace( - servicePrefix: String, - namespace: String, - ): Int { - return try { - secretStore().deleteNamespace(servicePrefix, namespace) - clearError() - STATUS_SUCCESS - } catch (cause: Throwable) { - captureError(cause) - } - } - - @JvmStatic - fun resolveRadrootsBaseRoot(): String? { - return try { - val path = secretStore().resolveRadrootsBaseRoot().absolutePath - clearError() - path - } catch (cause: Throwable) { - captureError(cause) - null - } - } - - @JvmStatic - fun beginUserPresenceVerification(reason: String): Int { - return try { - if (reason.isBlank()) { - throw RadRootsAndroidSecurityError.InvalidInput("verification reason must not be blank") - } - if (userPresenceVerificationPending) { - throw RadRootsAndroidSecurityError.InvalidInput("device authentication is already in progress") - } - val activity = currentActivity - ?: throw RadRootsAndroidSecurityError.InvalidInput("android security bridge has no active activity") - - clearError() - userPresenceVerificationPending = true - userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE - - RadRootsAndroidUserPresenceVerifier(activity).beginVerification( - reason = reason, - onSuccess = { - clearError() - userPresenceVerificationPending = false - userPresenceVerificationResult = USER_PRESENCE_RESULT_SUCCESS - }, - onFailure = { cause -> - lastErrorMessage = cause.message ?: cause.toString() - userPresenceVerificationPending = false - userPresenceVerificationResult = USER_PRESENCE_RESULT_ERROR - }, - ) - - STATUS_SUCCESS - } catch (cause: Throwable) { - userPresenceVerificationPending = false - userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE - captureError(cause) - } - } - - @JvmStatic - fun isUserPresenceVerificationPending(): Boolean = userPresenceVerificationPending - - @JvmStatic - fun takeUserPresenceVerificationResult(): Int { - val result = userPresenceVerificationResult - userPresenceVerificationResult = USER_PRESENCE_RESULT_NONE - return result - } - - @JvmStatic - fun takeLastErrorMessage(): String? { - val message = lastErrorMessage - lastErrorMessage = null - return message - } - - private fun secretStore(): RadRootsAndroidKeystoreSecretStore { - val context = applicationContext - ?: throw RadRootsAndroidSecurityError.InvalidInput("android security bridge is not initialized") - return RadRootsAndroidKeystoreSecretStore(context) - } - - private fun captureError(cause: Throwable): Int { - lastErrorMessage = cause.message ?: cause.toString() - return when (cause) { - is RadRootsAndroidSecurityError.NotFound -> STATUS_NOT_FOUND - is RadRootsAndroidSecurityError.InvalidInput -> STATUS_INVALID_INPUT - else -> STATUS_ERROR - } - } - - private fun clearError() { - lastErrorMessage = null - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityError.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityError.kt @@ -1,23 +0,0 @@ -package org.radroots.app.android.security - -sealed class RadRootsAndroidSecurityError( - message: String, - cause: Throwable? = null, -) : Exception(message, cause) { - class InvalidInput(message: String) : RadRootsAndroidSecurityError(message) - - class NotFound(message: String) : RadRootsAndroidSecurityError(message) - - class KeystoreFailure(message: String, cause: Throwable? = null) : - RadRootsAndroidSecurityError(message, cause) - - class StorageFailure(message: String, cause: Throwable? = null) : - RadRootsAndroidSecurityError(message, cause) - - class UserCancelled(message: String) : RadRootsAndroidSecurityError(message) - - class UserPresenceUnavailable(message: String) : RadRootsAndroidSecurityError(message) - - class UserPresenceFailure(message: String, cause: Throwable? = null) : - RadRootsAndroidSecurityError(message, cause) -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidStoragePaths.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidStoragePaths.kt @@ -1,99 +0,0 @@ -package org.radroots.app.android.security - -import android.content.Context -import java.io.File -import java.security.MessageDigest - -object RadRootsAndroidStoragePaths { - private const val rootDirName = "RadRoots" - private const val configDirName = "config" - private const val dataDirName = "data" - private const val secretsRootDirName = "secrets" - private const val appsDirName = "apps" - private const val appRuntimeDirName = "app" - private const val nostrDirName = "nostr" - private const val accountsFileName = "accounts.json" - - fun baseRoot(context: Context): File = baseRoot(context.noBackupFilesDir) - - fun baseRoot(baseDir: File): File = File(baseDir, rootDirName) - - fun appDataRoot(context: Context): File = appDataRoot(context.noBackupFilesDir) - - fun appDataRoot(baseDir: File): File = - File( - File( - File(baseRoot(baseDir), dataDirName), - appsDirName, - ), - appRuntimeDirName, - ) - - fun nostrRoot(context: Context): File = nostrRoot(context.noBackupFilesDir) - - fun nostrRoot(baseDir: File): File = File(appDataRoot(baseDir), nostrDirName) - - fun secretsDir(context: Context): File = secretsDir(context.noBackupFilesDir) - - fun secretsDir(baseDir: File): File = - File( - File( - File(baseRoot(baseDir), secretsRootDirName), - appsDirName, - ), - appRuntimeDirName, - ) - - fun accountsFile(context: Context): File = accountsFile(context.noBackupFilesDir) - - fun accountsFile(baseDir: File): File = File(nostrRoot(baseDir), accountsFileName) - - fun secretFile( - context: Context, - servicePrefix: String, - namespace: String, - name: String, - ): File = secretFile(context.noBackupFilesDir, servicePrefix, namespace, name) - - fun secretFile( - baseDir: File, - servicePrefix: String, - namespace: String, - name: String, - ): File = File( - secretsDir(baseDir), - "${secretNamespaceId(servicePrefix, namespace)}.${secretFileId(servicePrefix, namespace, name)}.bin", - ) - - fun legacySecretFile( - baseDir: File, - servicePrefix: String, - namespace: String, - name: String, - ): File = File(secretsDir(baseDir), "${secretFileId(servicePrefix, namespace, name)}.bin") - - fun secretNamespaceId(servicePrefix: String, namespace: String): String { - val digest = MessageDigest.getInstance("SHA-256") - val encoded = buildString { - append(servicePrefix) - append('\u0000') - append(namespace) - }.toByteArray(Charsets.UTF_8) - return digest.digest(encoded).joinToString("") { "%02x".format(it) } - } - - fun namespaceFilePrefix(servicePrefix: String, namespace: String): String = - "${secretNamespaceId(servicePrefix, namespace)}." - - fun secretFileId(servicePrefix: String, namespace: String, name: String): String { - val digest = MessageDigest.getInstance("SHA-256") - val encoded = buildString { - append(servicePrefix) - append('\u0000') - append(namespace) - append('\u0000') - append(name) - }.toByteArray(Charsets.UTF_8) - return digest.digest(encoded).joinToString("") { "%02x".format(it) } - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidUserPresenceVerifier.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/main/kotlin/org/radroots/app/android/security/RadRootsAndroidUserPresenceVerifier.kt @@ -1,149 +0,0 @@ -package org.radroots.app.android.security - -import android.app.KeyguardManager -import android.os.Build -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.fragment.app.FragmentActivity -import androidx.core.content.ContextCompat - -class RadRootsAndroidUserPresenceVerifier( - private val activity: FragmentActivity, -) { - fun beginVerification( - reason: String, - onSuccess: () -> Unit, - onFailure: (RadRootsAndroidSecurityError) -> Unit, - ) { - if (reason.isBlank()) { - onFailure(RadRootsAndroidSecurityError.InvalidInput("verification reason must not be blank")) - return - } - - val promptInfo = try { - buildPromptInfo(reason) - } catch (error: RadRootsAndroidSecurityError) { - onFailure(error) - return - } - - val executor = ContextCompat.getMainExecutor(activity) - val prompt = BiometricPrompt( - activity, - executor, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - onSuccess() - } - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - onFailure(mapAuthenticationError(errorCode, errString)) - } - - override fun onAuthenticationFailed() { - onFailure( - RadRootsAndroidSecurityError.UserPresenceFailure( - "device authentication failed", - ), - ) - } - }, - ) - - activity.runOnUiThread { - prompt.authenticate(promptInfo) - } - } - - private fun buildPromptInfo(reason: String): BiometricPrompt.PromptInfo { - ensureAuthenticationAvailable() - - val builder = BiometricPrompt.PromptInfo.Builder() - .setTitle("Rad Roots") - .setSubtitle("Authenticate to $reason") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - builder.setAllowedAuthenticators( - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL, - ) - } else if (deviceCredentialAvailable()) { - builder.setDeviceCredentialAllowed(true) - } else { - builder.setNegativeButtonText("Cancel") - } - - return builder.build() - } - - private fun ensureAuthenticationAvailable() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when ( - BiometricManager.from(activity).canAuthenticate( - BiometricManager.Authenticators.BIOMETRIC_STRONG or - BiometricManager.Authenticators.DEVICE_CREDENTIAL, - ) - ) { - BiometricManager.BIOMETRIC_SUCCESS -> return - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> - throw RadRootsAndroidSecurityError.UserPresenceUnavailable( - "no device authentication method is enrolled", - ) - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> - throw RadRootsAndroidSecurityError.UserPresenceUnavailable( - "device authentication is unavailable", - ) - else -> - throw RadRootsAndroidSecurityError.UserPresenceFailure( - "failed to prepare device authentication", - ) - } - } - - val biometricStatus = BiometricManager.from(activity).canAuthenticate() - if (biometricStatus == BiometricManager.BIOMETRIC_SUCCESS || deviceCredentialAvailable()) { - return - } - - throw when (biometricStatus) { - BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> - RadRootsAndroidSecurityError.UserPresenceUnavailable( - "no biometric or device credential is available", - ) - BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, - BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> - RadRootsAndroidSecurityError.UserPresenceUnavailable( - "device authentication is unavailable", - ) - else -> - RadRootsAndroidSecurityError.UserPresenceFailure( - "failed to prepare device authentication", - ) - } - } - - private fun deviceCredentialAvailable(): Boolean { - val keyguardManager = activity.getSystemService(KeyguardManager::class.java) - return keyguardManager?.isDeviceSecure == true - } - - private fun mapAuthenticationError( - errorCode: Int, - errString: CharSequence, - ): RadRootsAndroidSecurityError { - val message = errString.toString() - return when (errorCode) { - BiometricPrompt.ERROR_NEGATIVE_BUTTON, - BiometricPrompt.ERROR_USER_CANCELED, - BiometricPrompt.ERROR_CANCELED -> - RadRootsAndroidSecurityError.UserCancelled(message) - BiometricPrompt.ERROR_HW_NOT_PRESENT, - BiometricPrompt.ERROR_HW_UNAVAILABLE, - BiometricPrompt.ERROR_NO_BIOMETRICS, - BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> - RadRootsAndroidSecurityError.UserPresenceUnavailable(message) - else -> RadRootsAndroidSecurityError.UserPresenceFailure(message) - } - } -} diff --git a/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/test/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityTests.kt b/native/bridges/android/security/kotlin/RadRootsAndroidSecurity/src/test/kotlin/org/radroots/app/android/security/RadRootsAndroidSecurityTests.kt @@ -1,175 +0,0 @@ -package org.radroots.app.android.security - -import android.os.Build -import android.security.keystore.KeyProperties -import java.io.File -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class RadRootsAndroidSecurityTests { - @Test - fun secureLocalSecretPolicyDefaultsAreStable() { - val policy = RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET - - assertTrue(policy.deviceLocalOnly) - assertFalse(policy.userPresenceRequired) - assertTrue(policy.preferStrongBox) - } - - @Test - fun mobileNativeRootsUseNoBackupLayout() { - val baseDir = File("/data/user/0/org.radroots.app.android/no_backup") - - assertEquals( - File("/data/user/0/org.radroots.app.android/no_backup/RadRoots"), - RadRootsAndroidStoragePaths.baseRoot(baseDir), - ) - assertEquals( - File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app"), - RadRootsAndroidStoragePaths.appDataRoot(baseDir), - ) - assertEquals( - File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr"), - RadRootsAndroidStoragePaths.nostrRoot(baseDir), - ) - assertEquals( - File("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app/nostr/accounts.json"), - RadRootsAndroidStoragePaths.accountsFile(baseDir), - ) - } - - @Test - fun secretFileIdIsDeterministic() { - val first = RadRootsAndroidStoragePaths.secretFileId( - servicePrefix = "org.radroots.app.nostr", - namespace = "nostr", - name = "account-1", - ) - val second = RadRootsAndroidStoragePaths.secretFileId( - servicePrefix = "org.radroots.app.nostr", - namespace = "nostr", - name = "account-1", - ) - - assertEquals(first, second) - assertEquals(64, first.length) - } - - @Test - fun secretFileNamesCarryNamespacePrefix() { - val baseDir = File("/data/user/0/org.radroots.app.android/no_backup") - val path = RadRootsAndroidStoragePaths.secretFile( - baseDir = baseDir, - servicePrefix = "org.radroots.app.nostr", - namespace = "remote-signer", - name = "client-1", - ) - - assertTrue(path.name.endsWith(".bin")) - assertTrue( - path.name.startsWith( - "${RadRootsAndroidStoragePaths.secretNamespaceId("org.radroots.app.nostr", "remote-signer")}.", - ), - ) - } - - @Test - fun strongBoxIsRequestedOnlyWhenSupported() { - val policy = RadRootsAndroidSecretAccessPolicy.SECURE_LOCAL_SECRET - - assertTrue( - shouldRequestStrongBox( - policy = policy, - sdkInt = Build.VERSION_CODES.P, - hasStrongBoxFeature = true, - ), - ) - assertFalse( - shouldRequestStrongBox( - policy = policy, - sdkInt = Build.VERSION_CODES.O_MR1, - hasStrongBoxFeature = true, - ), - ) - assertFalse( - shouldRequestStrongBox( - policy = policy.copy(preferStrongBox = false), - sdkInt = Build.VERSION_CODES.P, - hasStrongBoxFeature = true, - ), - ) - assertFalse( - shouldRequestStrongBox( - policy = policy, - sdkInt = Build.VERSION_CODES.P, - hasStrongBoxFeature = false, - ), - ) - } - - @Test - fun securityLevelMappingPrefersVerifiedPlatformTier() { - assertEquals( - RadRootsAndroidKeySecurityLevel.STRONGBOX, - RadRootsAndroidKeySecurityLevels.fromPlatformValues( - sdkInt = Build.VERSION_CODES.S, - securityLevel = KeyProperties.SECURITY_LEVEL_STRONGBOX, - isInsideSecureHardware = true, - ), - ) - assertEquals( - RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT, - RadRootsAndroidKeySecurityLevels.fromPlatformValues( - sdkInt = Build.VERSION_CODES.S, - securityLevel = KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT, - isInsideSecureHardware = true, - ), - ) - assertEquals( - RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN, - RadRootsAndroidKeySecurityLevels.fromPlatformValues( - sdkInt = Build.VERSION_CODES.S, - securityLevel = KeyProperties.SECURITY_LEVEL_SOFTWARE, - isInsideSecureHardware = false, - ), - ) - assertEquals( - RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT, - RadRootsAndroidKeySecurityLevels.fromPlatformValues( - sdkInt = Build.VERSION_CODES.R, - securityLevel = null, - isInsideSecureHardware = true, - ), - ) - } - - @Test - fun strongBoxVerificationAcceptsOnlyBestAvailableTier() { - assertTrue( - acceptsStrongBoxVerificationResult( - sdkInt = Build.VERSION_CODES.S, - securityLevel = RadRootsAndroidKeySecurityLevel.STRONGBOX, - ), - ) - assertFalse( - acceptsStrongBoxVerificationResult( - sdkInt = Build.VERSION_CODES.S, - securityLevel = RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT, - ), - ) - assertTrue( - acceptsStrongBoxVerificationResult( - sdkInt = Build.VERSION_CODES.R, - securityLevel = RadRootsAndroidKeySecurityLevel.TRUSTED_ENVIRONMENT, - ), - ) - assertFalse( - acceptsStrongBoxVerificationResult( - sdkInt = Build.VERSION_CODES.R, - securityLevel = RadRootsAndroidKeySecurityLevel.SOFTWARE_OR_UNKNOWN, - ), - ) - } -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Package.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Package.swift @@ -1,42 +0,0 @@ -// swift-tools-version: 6.0 -import PackageDescription - -let package = Package( - name: "RadRootsAppleSecurity", - platforms: [ - .iOS(.v17), - .macOS(.v14) - ], - products: [ - .library( - name: "RadRootsAppleSecurity", - targets: ["RadRootsAppleSecurity"] - ), - .library( - name: "RadRootsAppleSecurityFFI", - type: .static, - targets: ["RadRootsAppleSecurityFFI"] - ), - .library( - name: "RadRootsAppleSecurityFFIDynamic", - type: .dynamic, - targets: ["RadRootsAppleSecurityFFI"] - ) - ], - targets: [ - .target( - name: "RadRootsAppleSecurity", - path: "Sources/RadRootsAppleSecurity" - ), - .target( - name: "RadRootsAppleSecurityFFI", - dependencies: ["RadRootsAppleSecurity"], - path: "Sources/RadRootsAppleSecurityFFI" - ), - .testTarget( - name: "RadRootsAppleSecurityTests", - dependencies: ["RadRootsAppleSecurity", "RadRootsAppleSecurityFFI"], - path: "Tests/RadRootsAppleSecurityTests" - ) - ] -) diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleKeychainSecretStore.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleKeychainSecretStore.swift @@ -1,123 +0,0 @@ -import Foundation -import Security - -public final class RadRootsAppleKeychainSecretStore: @unchecked Sendable { - public let servicePrefix: String - - public init(servicePrefix: String = "org.radroots.app.apple-security") { - self.servicePrefix = servicePrefix - } - - public func put( - _ value: Data, - for key: RadRootsAppleSecretKey, - policy: RadRootsAppleSecretAccessPolicy = .secureLocalSecret - ) throws { - try delete(key) - - let query = try writeQuery(for: key, value: value, policy: policy) - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecSuccess else { - throw Self.mapSecurityStatus(status, defaultMessage: "keychain write failed") - } - } - - public func get(_ key: RadRootsAppleSecretKey) throws -> Data? { - var query = baseQuery(for: key) - query[kSecReturnData as String] = true - query[kSecMatchLimit as String] = kSecMatchLimitOne - - var result: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &result) - if status == errSecItemNotFound { - return nil - } - guard status == errSecSuccess else { - throw Self.mapSecurityStatus(status, defaultMessage: "keychain read failed") - } - guard let data = result as? Data else { - throw RadRootsAppleSecurityError.permanentFailure( - "keychain read returned an invalid value type" - ) - } - return data - } - - public func contains(_ key: RadRootsAppleSecretKey) throws -> Bool { - try get(key) != nil - } - - public func delete(_ key: RadRootsAppleSecretKey) throws { - let status = SecItemDelete(baseQuery(for: key) as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - throw Self.mapSecurityStatus(status, defaultMessage: "keychain delete failed") - } - } - - public func deleteNamespace(_ namespace: String) throws { - guard !namespace.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty") - } - let status = SecItemDelete(namespaceQuery(namespace) as CFDictionary) - guard status == errSecSuccess || status == errSecItemNotFound else { - throw Self.mapSecurityStatus(status, defaultMessage: "keychain namespace delete failed") - } - } - - func baseQuery(for key: RadRootsAppleSecretKey) -> [String: Any] { - [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: key.serviceName(servicePrefix: servicePrefix), - kSecAttrAccount as String: key.name - ] - } - - func namespaceQuery(_ namespace: String) -> [String: Any] { - [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: "\(servicePrefix).\(namespace)" - ] - } - - func writeQuery( - for key: RadRootsAppleSecretKey, - value: Data, - policy: RadRootsAppleSecretAccessPolicy - ) throws -> [String: Any] { - var query = baseQuery(for: key) - query[kSecValueData as String] = value - query[kSecAttrAccessible as String] = accessibilityConstant(for: policy) - return query - } - - func accessibilityConstant(for policy: RadRootsAppleSecretAccessPolicy) -> CFString { - switch (policy.accessibility, policy.deviceLocalOnly) { - case (.whenUnlocked, true): - return kSecAttrAccessibleWhenUnlockedThisDeviceOnly - case (.whenUnlocked, false): - return kSecAttrAccessibleWhenUnlocked - case (.afterFirstUnlock, true): - return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - case (.afterFirstUnlock, false): - return kSecAttrAccessibleAfterFirstUnlock - } - } - - static func mapSecurityStatus( - _ status: OSStatus, - defaultMessage: String - ) -> RadRootsAppleSecurityError { - switch status { - case errSecAuthFailed: - return .permissionDenied(defaultMessage) - case errSecInteractionNotAllowed: - return .transientFailure(defaultMessage) - case errSecUserCanceled: - return .userCancelled(defaultMessage) - case errSecNotAvailable: - return .unavailable(defaultMessage) - default: - return .keychainStatus(status, defaultMessage) - } - } -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretAccessPolicy.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretAccessPolicy.swift @@ -1,28 +0,0 @@ -import Foundation - -public enum RadRootsAppleSecretAccessibility: Int32, Sendable { - case whenUnlocked = 0 - case afterFirstUnlock = 1 -} - -public struct RadRootsAppleSecretAccessPolicy: Sendable, Equatable { - public let accessibility: RadRootsAppleSecretAccessibility - public let deviceLocalOnly: Bool - public let userPresenceRequired: Bool - - public init( - accessibility: RadRootsAppleSecretAccessibility, - deviceLocalOnly: Bool, - userPresenceRequired: Bool - ) { - self.accessibility = accessibility - self.deviceLocalOnly = deviceLocalOnly - self.userPresenceRequired = userPresenceRequired - } - - public static let secureLocalSecret = Self( - accessibility: .whenUnlocked, - deviceLocalOnly: true, - userPresenceRequired: false - ) -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretKey.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecretKey.swift @@ -1,21 +0,0 @@ -import Foundation - -public struct RadRootsAppleSecretKey: Hashable, Sendable { - public let namespace: String - public let name: String - - public init(namespace: String, name: String) throws { - guard !namespace.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty") - } - guard !name.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("secret name cannot be empty") - } - self.namespace = namespace - self.name = name - } - - func serviceName(servicePrefix: String) -> String { - "\(servicePrefix).\(namespace)" - } -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecurityError.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleSecurityError.swift @@ -1,28 +0,0 @@ -import Foundation -import Security - -public enum RadRootsAppleSecurityError: Error, Sendable { - case invalidRequest(String) - case permissionDenied(String) - case userCancelled(String) - case unavailable(String) - case transientFailure(String) - case permanentFailure(String) - case keychainStatus(OSStatus, String) -} - -extension RadRootsAppleSecurityError: LocalizedError { - public var errorDescription: String? { - switch self { - case let .invalidRequest(message), - let .permissionDenied(message), - let .userCancelled(message), - let .unavailable(message), - let .transientFailure(message), - let .permanentFailure(message): - return message - case let .keychainStatus(status, message): - return "\(message) (status \(status))" - } - } -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleUserPresence.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurity/RadRootsAppleUserPresence.swift @@ -1,197 +0,0 @@ -import Foundation - -#if canImport(LocalAuthentication) -import LocalAuthentication -#endif - -public enum RadRootsAppleUserPresencePolicy: Sendable { - case deviceOwnerAuthentication - case deviceOwnerAuthenticationWithBiometrics -} - -public enum RadRootsAppleUserPresenceSupport: Sendable { - case none - case deviceCredential - case biometricsOrDeviceCredential -} - -public enum RadRootsAppleBiometryKind: Sendable { - case none - case touchID - case faceID - case opticID - case unknown -} - -public struct RadRootsAppleUserPresenceStatus: Sendable { - public let support: RadRootsAppleUserPresenceSupport - public let biometryKind: RadRootsAppleBiometryKind - public let canEvaluateDeviceCredential: Bool - public let canEvaluateBiometrics: Bool - - public init( - support: RadRootsAppleUserPresenceSupport, - biometryKind: RadRootsAppleBiometryKind, - canEvaluateDeviceCredential: Bool, - canEvaluateBiometrics: Bool - ) { - self.support = support - self.biometryKind = biometryKind - self.canEvaluateDeviceCredential = canEvaluateDeviceCredential - self.canEvaluateBiometrics = canEvaluateBiometrics - } -} - -public actor RadRootsAppleUserPresence { - public init() {} - - public static func verifySync( - reason: String, - policy: RadRootsAppleUserPresencePolicy = .deviceOwnerAuthentication - ) throws -> Bool { - #if canImport(LocalAuthentication) - let context = LAContext() - let lock = NSLock() - let semaphore = DispatchSemaphore(value: 0) - var result: Result<Bool, Error>? - - context.evaluatePolicy( - Self.makePolicy(policy), - localizedReason: reason - ) { success, error in - lock.lock() - if let error { - result = .failure(Self.adapt(error: error)) - } else { - result = .success(success) - } - lock.unlock() - semaphore.signal() - } - - semaphore.wait() - - lock.lock() - defer { lock.unlock() } - return try result?.get() ?? { - throw RadRootsAppleSecurityError.transientFailure( - "local authentication did not return a result" - ) - }() - #else - throw RadRootsAppleSecurityError.unavailable("local authentication is unavailable") - #endif - } - - public func currentStatus() -> RadRootsAppleUserPresenceStatus { - #if canImport(LocalAuthentication) - let context = LAContext() - return Self.makeStatus(context: context) - #else - return RadRootsAppleUserPresenceStatus( - support: .none, - biometryKind: .none, - canEvaluateDeviceCredential: false, - canEvaluateBiometrics: false - ) - #endif - } - - public func verify( - reason: String, - policy: RadRootsAppleUserPresencePolicy = .deviceOwnerAuthentication - ) async throws -> Bool { - #if canImport(LocalAuthentication) - let context = LAContext() - return try await withCheckedThrowingContinuation { continuation in - context.evaluatePolicy( - Self.makePolicy(policy), - localizedReason: reason - ) { success, error in - if let error { - continuation.resume(throwing: Self.adapt(error: error)) - } else { - continuation.resume(returning: success) - } - } - } - #else - throw RadRootsAppleSecurityError.unavailable("local authentication is unavailable") - #endif - } - - #if canImport(LocalAuthentication) - private static func makePolicy(_ policy: RadRootsAppleUserPresencePolicy) -> LAPolicy { - switch policy { - case .deviceOwnerAuthentication: - return .deviceOwnerAuthentication - case .deviceOwnerAuthenticationWithBiometrics: - return .deviceOwnerAuthenticationWithBiometrics - } - } - - private static func makeStatus(context: LAContext) -> RadRootsAppleUserPresenceStatus { - var biometricsError: NSError? - let canEvaluateBiometrics = context.canEvaluatePolicy( - .deviceOwnerAuthenticationWithBiometrics, - error: &biometricsError - ) - - var deviceCredentialError: NSError? - let canEvaluateDeviceCredential = context.canEvaluatePolicy( - .deviceOwnerAuthentication, - error: &deviceCredentialError - ) - - let support: RadRootsAppleUserPresenceSupport - if canEvaluateBiometrics { - support = .biometricsOrDeviceCredential - } else if canEvaluateDeviceCredential { - support = .deviceCredential - } else { - support = .none - } - - return RadRootsAppleUserPresenceStatus( - support: support, - biometryKind: makeBiometryKind(context.biometryType), - canEvaluateDeviceCredential: canEvaluateDeviceCredential, - canEvaluateBiometrics: canEvaluateBiometrics - ) - } - - private static func makeBiometryKind(_ biometryType: LABiometryType) -> RadRootsAppleBiometryKind { - switch biometryType { - case .none: - return .none - case .touchID: - return .touchID - case .faceID: - return .faceID - case .opticID: - return .opticID - @unknown default: - return .unknown - } - } - - private static func adapt(error: Error) -> RadRootsAppleSecurityError { - if let laError = error as? LAError { - switch laError.code { - case .userCancel, .userFallback: - return .userCancelled(laError.localizedDescription) - case .appCancel, .systemCancel, .notInteractive: - return .transientFailure(laError.localizedDescription) - case .biometryNotAvailable, .biometryNotEnrolled, .passcodeNotSet: - return .unavailable(laError.localizedDescription) - case .authenticationFailed: - return .permissionDenied(laError.localizedDescription) - default: - return .permanentFailure(laError.localizedDescription) - } - } - - return .permanentFailure(error.localizedDescription) - } - #endif -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurityFFI/RadRootsAppleSecurityFFI.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Sources/RadRootsAppleSecurityFFI/RadRootsAppleSecurityFFI.swift @@ -1,250 +0,0 @@ -import Foundation -import RadRootsAppleSecurity - -private let defaultServicePrefix = "org.radroots.app.apple-security" - -private enum RadRootsAppleFFIStatus: Int32 { - case success = 0 - case notFound = 1 - case invalidInput = 2 - case error = 3 -} - -@_cdecl("radroots_apple_secret_store_put") -public func radroots_apple_secret_store_put( - _ servicePrefix: UnsafePointer<CChar>?, - _ namespace: UnsafePointer<CChar>?, - _ name: UnsafePointer<CChar>?, - _ valuePtr: UnsafePointer<UInt8>?, - _ valueLen: Int, - _ accessibilityRaw: Int32, - _ deviceLocalOnlyRaw: Int32, - _ userPresenceRequiredRaw: Int32, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - let store = try makeStore(servicePrefix: servicePrefix) - let key = try makeKey(namespace: namespace, name: name) - let policy = try makePolicy( - accessibilityRaw: accessibilityRaw, - deviceLocalOnlyRaw: deviceLocalOnlyRaw, - userPresenceRequiredRaw: userPresenceRequiredRaw - ) - guard let valuePtr else { - throw RadRootsAppleSecurityError.invalidRequest("secret value pointer cannot be null") - } - let value = Data(bytes: valuePtr, count: valueLen) - try store.put(value, for: key, policy: policy) - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_secret_store_get") -public func radroots_apple_secret_store_get( - _ servicePrefix: UnsafePointer<CChar>?, - _ namespace: UnsafePointer<CChar>?, - _ name: UnsafePointer<CChar>?, - _ valueOut: UnsafeMutablePointer<UnsafeMutablePointer<UInt8>?>?, - _ valueLenOut: UnsafeMutablePointer<Int>?, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - guard let valueOut, let valueLenOut else { - throw RadRootsAppleSecurityError.invalidRequest("output buffers cannot be null") - } - let store = try makeStore(servicePrefix: servicePrefix) - let key = try makeKey(namespace: namespace, name: name) - guard let value = try store.get(key) else { - valueOut.pointee = nil - valueLenOut.pointee = 0 - return RadRootsAppleFFIStatus.notFound.rawValue - } - - let output = UnsafeMutablePointer<UInt8>.allocate(capacity: value.count) - value.copyBytes(to: output, count: value.count) - valueOut.pointee = output - valueLenOut.pointee = value.count - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_secret_store_contains") -public func radroots_apple_secret_store_contains( - _ servicePrefix: UnsafePointer<CChar>?, - _ namespace: UnsafePointer<CChar>?, - _ name: UnsafePointer<CChar>?, - _ containsOut: UnsafeMutablePointer<Int32>?, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - guard let containsOut else { - throw RadRootsAppleSecurityError.invalidRequest("contains output cannot be null") - } - let store = try makeStore(servicePrefix: servicePrefix) - let key = try makeKey(namespace: namespace, name: name) - containsOut.pointee = try store.contains(key) ? 1 : 0 - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_secret_store_delete") -public func radroots_apple_secret_store_delete( - _ servicePrefix: UnsafePointer<CChar>?, - _ namespace: UnsafePointer<CChar>?, - _ name: UnsafePointer<CChar>?, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - let store = try makeStore(servicePrefix: servicePrefix) - let key = try makeKey(namespace: namespace, name: name) - try store.delete(key) - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_secret_store_delete_namespace") -public func radroots_apple_secret_store_delete_namespace( - _ servicePrefix: UnsafePointer<CChar>?, - _ namespace: UnsafePointer<CChar>?, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - let store = try makeStore(servicePrefix: servicePrefix) - let namespace = try makeNamespace(namespace) - try store.deleteNamespace(namespace) - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_user_presence_verify") -public func radroots_apple_user_presence_verify( - _ reason: UnsafePointer<CChar>?, - _ errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) -> Int32 { - do { - guard let reasonPointer = reason else { - throw RadRootsAppleSecurityError.invalidRequest("verification reason is required") - } - let reason = String(cString: reasonPointer) - guard !reason.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("verification reason cannot be empty") - } - guard try RadRootsAppleUserPresence.verifySync(reason: reason) else { - throw RadRootsAppleSecurityError.permissionDenied( - "local authentication did not authorize access" - ) - } - return RadRootsAppleFFIStatus.success.rawValue - } catch { - setError(error, into: errorOut) - return statusForError(error) - } -} - -@_cdecl("radroots_apple_buffer_free") -public func radroots_apple_buffer_free( - _ buffer: UnsafeMutablePointer<UInt8>?, - _ length: Int -) { - guard let buffer else { - return - } - buffer.deallocate() - _ = length -} - -@_cdecl("radroots_apple_c_string_free") -public func radroots_apple_c_string_free(_ string: UnsafeMutablePointer<CChar>?) { - string?.deallocate() -} - -private func makeStore( - servicePrefix: UnsafePointer<CChar>? -) throws -> RadRootsAppleKeychainSecretStore { - let service = servicePrefix.map(String.init(cString:)) ?? defaultServicePrefix - guard !service.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("service prefix cannot be empty") - } - return RadRootsAppleKeychainSecretStore(servicePrefix: service) -} - -private func makeKey( - namespace: UnsafePointer<CChar>?, - name: UnsafePointer<CChar>? -) throws -> RadRootsAppleSecretKey { - let namespaceValue = try makeNamespace(namespace) - guard let name else { - throw RadRootsAppleSecurityError.invalidRequest("secret namespace and name are required") - } - return try RadRootsAppleSecretKey( - namespace: namespaceValue, - name: String(cString: name) - ) -} - -private func makeNamespace( - _ namespace: UnsafePointer<CChar>? -) throws -> String { - guard let namespace else { - throw RadRootsAppleSecurityError.invalidRequest("secret namespace is required") - } - let value = String(cString: namespace) - guard !value.isEmpty else { - throw RadRootsAppleSecurityError.invalidRequest("secret namespace cannot be empty") - } - return value -} - -private func makePolicy( - accessibilityRaw: Int32, - deviceLocalOnlyRaw: Int32, - userPresenceRequiredRaw: Int32 -) throws -> RadRootsAppleSecretAccessPolicy { - guard let accessibility = RadRootsAppleSecretAccessibility(rawValue: accessibilityRaw) else { - throw RadRootsAppleSecurityError.invalidRequest("invalid accessibility value") - } - return RadRootsAppleSecretAccessPolicy( - accessibility: accessibility, - deviceLocalOnly: deviceLocalOnlyRaw != 0, - userPresenceRequired: userPresenceRequiredRaw != 0 - ) -} - -private func setError( - _ error: Error, - into errorOut: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>? -) { - guard let errorOut else { - return - } - errorOut.pointee = duplicateCString(error.localizedDescription) -} - -private func statusForError(_ error: Error) -> Int32 { - if case RadRootsAppleSecurityError.invalidRequest = error { - return RadRootsAppleFFIStatus.invalidInput.rawValue - } - return RadRootsAppleFFIStatus.error.rawValue -} - -private func duplicateCString(_ value: String) -> UnsafeMutablePointer<CChar>? { - let bytes = Array(value.utf8CString) - let pointer = UnsafeMutablePointer<CChar>.allocate(capacity: bytes.count) - pointer.initialize(from: bytes, count: bytes.count) - return pointer -} diff --git a/native/bridges/apple/security/swift/RadRootsAppleSecurity/Tests/RadRootsAppleSecurityTests/RadRootsAppleSecurityTests.swift b/native/bridges/apple/security/swift/RadRootsAppleSecurity/Tests/RadRootsAppleSecurityTests/RadRootsAppleSecurityTests.swift @@ -1,60 +0,0 @@ -import Foundation -import Security -@testable import RadRootsAppleSecurity -@testable import RadRootsAppleSecurityFFI -import Testing - -struct RadRootsAppleSecurityTests { - @Test - func secretKeyRejectsEmptyNamespace() throws { - #expect(throws: RadRootsAppleSecurityError.self) { - _ = try RadRootsAppleSecretKey(namespace: "", name: "secret") - } - } - - @Test - func secretKeyRejectsEmptyName() throws { - #expect(throws: RadRootsAppleSecurityError.self) { - _ = try RadRootsAppleSecretKey(namespace: "nostr", name: "") - } - } - - @Test - func baseQueryUsesStableServicePrefixAndAccountName() throws { - let store = RadRootsAppleKeychainSecretStore(servicePrefix: "org.radroots.app.nostr") - let key = try RadRootsAppleSecretKey(namespace: "accounts", name: "account-1") - - let query = store.baseQuery(for: key) - - #expect(query[kSecAttrService as String] as? String == "org.radroots.app.nostr.accounts") - #expect(query[kSecAttrAccount as String] as? String == "account-1") - #expect(query[kSecClass as String] != nil) - } - - @Test - func secureLocalSecretDefaultsToDeviceLocalWhenUnlocked() { - let policy = RadRootsAppleSecretAccessPolicy.secureLocalSecret - - #expect(policy.accessibility == .whenUnlocked) - #expect(policy.deviceLocalOnly) - #expect(!policy.userPresenceRequired) - } - - @Test - func accessibilityConstantMatchesPolicy() { - let store = RadRootsAppleKeychainSecretStore() - let localPolicy = RadRootsAppleSecretAccessPolicy( - accessibility: .whenUnlocked, - deviceLocalOnly: true, - userPresenceRequired: false - ) - let syncedPolicy = RadRootsAppleSecretAccessPolicy( - accessibility: .afterFirstUnlock, - deviceLocalOnly: false, - userPresenceRequired: false - ) - - #expect(store.accessibilityConstant(for: localPolicy) == kSecAttrAccessibleWhenUnlockedThisDeviceOnly) - #expect(store.accessibilityConstant(for: syncedPolicy) == kSecAttrAccessibleAfterFirstUnlock) - } -} diff --git a/platforms/android/Scripts/android_toolchain_config.sh b/platforms/android/Scripts/android_toolchain_config.sh @@ -1,83 +0,0 @@ -#!/usr/bin/env bash - -android_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -android_dir="$(cd "${android_script_dir}/.." && pwd -P)" -app_root="$(cd "${android_dir}/../.." && pwd -P)" - -android_tooling_dir="${android_dir}/.tooling" -android_download_dir="${android_tooling_dir}/downloads" -android_sdk_dir="${android_tooling_dir}/android-sdk" -android_gradle_user_home="${android_tooling_dir}/gradle-user-home" -android_user_home="${android_tooling_dir}/android-user-home" -android_emulator_home="${android_tooling_dir}/emulator-home" -android_avd_home="${android_tooling_dir}/avd" -android_cargo_install_root="${android_tooling_dir}/cargo" -android_cargo_bin_dir="${android_cargo_install_root}/bin" -android_local_properties_path="${android_dir}/local.properties" - -android_sdk_api_level="34" -android_build_tools_version="34.0.0" -android_ndk_version="26.1.10909125" -android_gradle_version="8.7" -android_gradle_distribution_sha256="544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d" -android_cmdline_tools_version="14742923" -android_cargo_ndk_version="4.1.2" - -android_gradle_home="${android_tooling_dir}/gradle/gradle-${android_gradle_version}" -android_gradle_bin="${android_gradle_home}/bin/gradle" -android_sdkmanager_bin="${android_sdk_dir}/cmdline-tools/latest/bin/sdkmanager" -android_avdmanager_bin="${android_sdk_dir}/cmdline-tools/latest/bin/avdmanager" -android_ndk_dir="${android_sdk_dir}/ndk/${android_ndk_version}" -android_emulator_bin="${android_sdk_dir}/emulator/emulator" -android_adb_bin="${android_sdk_dir}/platform-tools/adb" -android_cargo_ndk_bin="${android_cargo_bin_dir}/cargo-ndk" - -android_rust_target="aarch64-linux-android" -android_abi="arm64-v8a" - -android_sdk_packages=( - "platform-tools" - "platforms;android-${android_sdk_api_level}" - "build-tools;${android_build_tools_version}" - "ndk;${android_ndk_version}" -) - -android_gradle_distribution_url() { - echo "https://services.gradle.org/distributions/gradle-${android_gradle_version}-bin.zip" -} - -android_cmdline_tools_platform() { - case "$(uname -s)" in - Darwin) - echo "mac" - ;; - Linux) - echo "linux" - ;; - *) - echo "unsupported" - ;; - esac -} - -android_cmdline_tools_zip_name() { - local platform_name - platform_name="$(android_cmdline_tools_platform)" - echo "commandlinetools-${platform_name}-${android_cmdline_tools_version}_latest.zip" -} - -android_cmdline_tools_url() { - echo "https://dl.google.com/android/repository/$(android_cmdline_tools_zip_name)" -} - -android_emulator_system_image_package() { - echo "system-images;android-${android_sdk_api_level};google_apis;${android_abi}" -} - -android_emulator_packages() { - printf '%s\n' "emulator" "$(android_emulator_system_image_package)" -} - -android_avd_name() { - echo "RadRoots_API_${android_sdk_api_level}" -} diff --git a/platforms/android/Scripts/bootstrap_android_toolchain.sh b/platforms/android/Scripts/bootstrap_android_toolchain.sh @@ -1,226 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -source "${script_dir}/android_toolchain_config.sh" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -require_java_17() { - require_command java - - local version_output - version_output="$(java -version 2>&1 | head -n 1)" - local java_major - java_major="$(echo "${version_output}" | sed -E 's/.*version "([0-9]+).*/\1/')" - if [[ -z "${java_major}" || "${java_major}" -lt 17 ]]; then - echo "android bootstrap requires java 17 or newer" >&2 - exit 1 - fi -} - -checksum_file() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' - return - fi - - if command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$1" | awk '{print $1}' - return - fi - - echo "missing required command: sha256sum or shasum" >&2 - exit 1 -} - -download_if_missing() { - local url="$1" - local destination="$2" - - if [[ -f "${destination}" ]]; then - return - fi - - mkdir -p "$(dirname "${destination}")" - curl -fsSL "${url}" -o "${destination}" -} - -validate_zip_archive() { - unzip -tqq "$1" >/dev/null 2>&1 -} - -ensure_valid_zip_download() { - local url="$1" - local destination="$2" - - download_if_missing "${url}" "${destination}" - if validate_zip_archive "${destination}"; then - return - fi - - rm -f "${destination}" - download_if_missing "${url}" "${destination}" - if ! validate_zip_archive "${destination}"; then - echo "invalid zip archive: ${destination}" >&2 - exit 1 - fi -} - -ensure_gradle_distribution() { - if [[ -x "${android_gradle_bin}" ]]; then - return - fi - - local gradle_zip="${android_download_dir}/gradle-${android_gradle_version}-bin.zip" - ensure_valid_zip_download "$(android_gradle_distribution_url)" "${gradle_zip}" - - local actual_checksum - actual_checksum="$(checksum_file "${gradle_zip}")" - if [[ "${actual_checksum}" != "${android_gradle_distribution_sha256}" ]]; then - rm -f "${gradle_zip}" - ensure_valid_zip_download "$(android_gradle_distribution_url)" "${gradle_zip}" - actual_checksum="$(checksum_file "${gradle_zip}")" - if [[ "${actual_checksum}" != "${android_gradle_distribution_sha256}" ]]; then - echo "gradle distribution checksum mismatch" >&2 - exit 1 - fi - fi - - rm -rf "$(dirname "${android_gradle_home}")" - mkdir -p "$(dirname "${android_gradle_home}")" - unzip -q "${gradle_zip}" -d "$(dirname "${android_gradle_home}")" -} - -ensure_android_cmdline_tools() { - if [[ -x "${android_sdkmanager_bin}" ]]; then - return - fi - - local platform_name - platform_name="$(android_cmdline_tools_platform)" - if [[ "${platform_name}" == "unsupported" ]]; then - echo "android bootstrap supports only darwin and linux hosts" >&2 - exit 1 - fi - - local cmdline_zip="${android_download_dir}/$(android_cmdline_tools_zip_name)" - local tmp_dir="${android_tooling_dir}/tmp/cmdline-tools" - - ensure_valid_zip_download "$(android_cmdline_tools_url)" "${cmdline_zip}" - - rm -rf "${tmp_dir}" "${android_sdk_dir}/cmdline-tools/latest" - mkdir -p "${tmp_dir}" "${android_sdk_dir}/cmdline-tools/latest" - unzip -q "${cmdline_zip}" -d "${tmp_dir}" - mv "${tmp_dir}/cmdline-tools/"* "${android_sdk_dir}/cmdline-tools/latest/" - rm -rf "${tmp_dir}" -} - -accept_android_licenses() { - set +o pipefail - yes | "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" --licenses >/dev/null - set -o pipefail -} - -ensure_android_sdk_packages() { - accept_android_licenses - "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" "${android_sdk_packages[@]}" -} - -ensure_android_emulator_packages() { - accept_android_licenses - local packages=() - while IFS= read -r package; do - packages+=("${package}") - done < <(android_emulator_packages) - "${android_sdkmanager_bin}" --sdk_root="${android_sdk_dir}" "${packages[@]}" -} - -ensure_cargo_ndk() { - if [[ -x "${android_cargo_ndk_bin}" ]]; then - local installed_version - installed_version="$(PATH="${android_cargo_bin_dir}:${PATH}" cargo ndk --version | awk '{print $2}')" - if [[ "${installed_version}" == "${android_cargo_ndk_version}" ]]; then - return - fi - fi - - cargo install \ - --locked \ - --force \ - --root "${android_cargo_install_root}" \ - --version "${android_cargo_ndk_version}" \ - cargo-ndk -} - -ensure_rust_target() { - if rustup target list --installed | grep -Fx "${android_rust_target}" >/dev/null 2>&1; then - return - fi - rustup target add "${android_rust_target}" -} - -write_local_properties() { - cat <<EOF > "${android_local_properties_path}" -sdk.dir=${android_sdk_dir} -EOF -} - -main() { - local with_emulator="false" - local print_gradle_bin="false" - - while [[ $# -gt 0 ]]; do - case "$1" in - --with-emulator) - with_emulator="true" - shift - ;; - --print-gradle-bin) - print_gradle_bin="true" - shift - ;; - *) - echo "unknown bootstrap option: $1" >&2 - exit 1 - ;; - esac - done - - require_command curl - require_command unzip - require_command cargo - require_command rustup - require_java_17 - - mkdir -p \ - "${android_tooling_dir}" \ - "${android_download_dir}" \ - "${android_gradle_user_home}" \ - "${android_user_home}" \ - "${android_emulator_home}" \ - "${android_avd_home}" \ - "${android_cargo_install_root}" - - ensure_gradle_distribution - ensure_android_cmdline_tools - ensure_android_sdk_packages - if [[ "${with_emulator}" == "true" ]]; then - ensure_android_emulator_packages - fi - ensure_cargo_ndk - ensure_rust_target - write_local_properties - - if [[ "${print_gradle_bin}" == "true" ]]; then - printf '%s\n' "${android_gradle_bin}" - fi -} - -main "$@" diff --git a/platforms/android/Scripts/build_rust_android.sh b/platforms/android/Scripts/build_rust_android.sh @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -source "${script_dir}/android_toolchain_config.sh" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -profile_for_build_type() { - case "${1}" in - Release) - echo "release" - ;; - *) - echo "debug" - ;; - esac -} - -missing_bootstrap() { - echo "android build requires bootstrapped local toolchain files under platforms/android/.tooling" >&2 - exit 1 -} - -require_command cargo -require_command rustup - -if [[ ! -d "${android_sdk_dir}" || ! -d "${android_ndk_dir}" || ! -x "${android_cargo_ndk_bin}" ]]; then - missing_bootstrap -fi - -if ! rustup target list --installed | grep -Fx "${android_rust_target}" >/dev/null 2>&1; then - missing_bootstrap -fi - -build_type="${1:-Debug}" -profile="$(profile_for_build_type "${build_type}")" - -export PATH="${android_cargo_bin_dir}:${PATH}" -export ANDROID_HOME="${android_sdk_dir}" -export ANDROID_SDK_ROOT="${android_sdk_dir}" -export ANDROID_NDK_HOME="${android_ndk_dir}" -export ANDROID_NDK_ROOT="${android_ndk_dir}" -export ANDROID_USER_HOME="${android_user_home}" - -cargo_args=( - ndk - -t "${android_abi}" - -o "${app_root}/target/android/jniLibs" - build - --manifest-path "${app_root}/Cargo.toml" - -p radroots_app_android -) - -if [[ "${profile}" == "release" ]]; then - cargo_args+=(--release) -fi - -cargo "${cargo_args[@]}" diff --git a/platforms/android/app/build.gradle.kts b/platforms/android/app/build.gradle.kts @@ -1,87 +0,0 @@ -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") -} - -val rustBuildScript = file("../Scripts/build_rust_android.sh") -val rustJniLibsDir = file("../../../target/android/jniLibs") -val rustInputs = files( - "../../../Cargo.toml", - "../../../Cargo.lock", - rustBuildScript, - fileTree("../../../crates/shared/core"), - fileTree("../../../crates/shared/remote_signer"), - fileTree("../../../crates/bridges/android/security"), - fileTree("../../../crates/launchers/android"), -) - -android { - namespace = "org.radroots.app.android" - compileSdk = 34 - ndkVersion = "26.1.10909125" - - defaultConfig { - applicationId = "org.radroots.app.android" - minSdk = 26 - targetSdk = 34 - versionCode = 1 - versionName = "0.1.0" - - ndk { - abiFilters += "arm64-v8a" - } - } - - buildTypes { - debug {} - release { - isMinifyEnabled = false - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - sourceSets { - getByName("main") { - jniLibs.srcDir(rustJniLibsDir) - assets.srcDir("../../../assets") - } - } -} - -val buildRustDebug = tasks.register("buildRustDebug", org.gradle.api.tasks.Exec::class) { - workingDir = rootDir - commandLine("bash", rustBuildScript.absolutePath, "Debug") - inputs.files(rustInputs) - outputs.dir(rustJniLibsDir) -} - -val buildRustRelease = tasks.register("buildRustRelease", org.gradle.api.tasks.Exec::class) { - workingDir = rootDir - commandLine("bash", rustBuildScript.absolutePath, "Release") - inputs.files(rustInputs) - outputs.dir(rustJniLibsDir) -} - -afterEvaluate { - tasks.named("preDebugBuild").configure { - dependsOn(buildRustDebug) - } - tasks.named("preReleaseBuild").configure { - dependsOn(buildRustRelease) - } -} - -dependencies { - implementation("androidx.games:games-activity:2.0.2") - implementation("androidx.appcompat:appcompat:1.7.0") - implementation("androidx.core:core-ktx:1.13.1") - implementation(project(":radrootsAndroidSecurity")) -} diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <application - android:allowBackup="false" - android:icon="@drawable/radroots_logo" - android:label="@string/app_name" - android:roundIcon="@drawable/radroots_logo" - android:supportsRtl="true" - android:theme="@style/Theme.RadRoots"> - <activity - android:name=".MainActivity" - android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|keyboard|smallestScreenSize|uiMode|fontScale" - android:exported="true" - android:launchMode="singleTask" - android:windowSoftInputMode="adjustResize"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> - </intent-filter> - - <meta-data - android:name="android.app.lib_name" - android:value="radroots_app_android" /> - </activity> - </application> - <uses-permission android:name="android.permission.INTERNET" /> -</manifest> diff --git a/platforms/android/app/src/main/kotlin/org/radroots/app/android/MainActivity.kt b/platforms/android/app/src/main/kotlin/org/radroots/app/android/MainActivity.kt @@ -1,13 +0,0 @@ -package org.radroots.app.android - -import android.os.Bundle -import com.google.androidgamesdk.GameActivity -import org.radroots.app.android.security.RadRootsAndroidSecurityBridge - -class MainActivity : GameActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - RadRootsAndroidAppBridge.initialize(this) - RadRootsAndroidSecurityBridge.initialize(this) - super.onCreate(savedInstanceState) - } -} diff --git a/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt b/platforms/android/app/src/main/kotlin/org/radroots/app/android/RadRootsAndroidAppBridge.kt @@ -1,129 +0,0 @@ -package org.radroots.app.android - -import android.content.Context -import java.io.File -import java.io.FileNotFoundException - -object RadRootsAndroidAppBridge { - private const val GEOCODER_ASSET_PATH = "geocoder/geonames.db" - private const val GEOCODER_REVISION_ASSET_PATH = "geocoder/geonames.revision" - private const val GEOCODER_FILE_NAME = "geonames.db" - private const val GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET = 1 - private const val GEOCODER_ERROR_KIND_INITIALIZATION_FAILED = 2 - private const val GEOCODER_ERROR_KIND_INTERNAL_ERROR = 3 - - @Volatile - private var appContext: Context? = null - - @Volatile - private var lastErrorMessage: String? = null - - @Volatile - private var lastErrorKind: Int = 0 - - @JvmStatic - fun initialize(context: Context) { - appContext = context.applicationContext - } - - @JvmStatic - @Synchronized - fun stageOfflineGeocoderAsset(): String? { - val context = appContext - ?: return fail( - GEOCODER_ERROR_KIND_INTERNAL_ERROR, - "android app bridge is not initialized", - ) - val revision = loadGeocoderRevision(context) ?: return null - val targetDir = stagedGeocoderDirectory(context, revision) - if (!targetDir.exists() && !targetDir.mkdirs()) { - return fail( - GEOCODER_ERROR_KIND_INITIALIZATION_FAILED, - "failed to create android geocoder directory: ${targetDir.absolutePath}", - ) - } - - val targetFile = File(targetDir, GEOCODER_FILE_NAME) - if (targetFile.isFile) { - lastErrorMessage = null - lastErrorKind = 0 - return targetFile.absolutePath - } - return try { - context.assets.open(GEOCODER_ASSET_PATH).use { input -> - targetFile.outputStream().use { output -> - input.copyTo(output) - } - } - lastErrorMessage = null - lastErrorKind = 0 - targetFile.absolutePath - } catch (_: FileNotFoundException) { - fail( - GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET, - "android bundled geocoder asset missing at assets/$GEOCODER_ASSET_PATH", - ) - } catch (source: Exception) { - fail( - GEOCODER_ERROR_KIND_INITIALIZATION_FAILED, - "failed to stage android geocoder asset: ${source.message ?: source.javaClass.simpleName}", - ) - } - } - - private fun loadGeocoderRevision(context: Context): String? { - val revision = try { - context.assets.open(GEOCODER_REVISION_ASSET_PATH).bufferedReader().use { it.readText() } - } catch (_: FileNotFoundException) { - return fail( - GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET, - "android bundled geocoder revision asset missing at assets/$GEOCODER_REVISION_ASSET_PATH", - ) - } catch (source: Exception) { - return fail( - GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET, - "failed to read android geocoder revision asset at assets/$GEOCODER_REVISION_ASSET_PATH: ${source.message ?: source.javaClass.simpleName}", - ) - }.trim() - - if (!isValidRevision(revision)) { - return fail( - GEOCODER_ERROR_KIND_MISSING_BUILD_ASSET, - "android bundled geocoder revision asset invalid at assets/$GEOCODER_REVISION_ASSET_PATH", - ) - } - - return revision - } - - private fun isValidRevision(revision: String): Boolean { - return revision.length == 64 && revision.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' } - } - - private fun stagedGeocoderDirectory(context: Context, revision: String): File { - return File(context.noBackupFilesDir, "RadRoots/app/android/geocoder/$revision") - } - - @JvmStatic - @Synchronized - fun takeLastErrorKind(): Int { - val value = lastErrorKind - lastErrorKind = 0 - return value - } - - @JvmStatic - @Synchronized - fun takeLastErrorMessage(): String? { - val value = lastErrorMessage - lastErrorMessage = null - return value - } - - @Synchronized - private fun fail(kind: Int, message: String): String? { - lastErrorKind = kind - lastErrorMessage = message - return null - } -} diff --git a/platforms/android/app/src/main/res/drawable-nodpi/radroots_logo.png b/platforms/android/app/src/main/res/drawable-nodpi/radroots_logo.png Binary files differ. diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="app_name">Rad Roots</string> -</resources> diff --git a/platforms/android/app/src/main/res/values/themes.xml b/platforms/android/app/src/main/res/values/themes.xml @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <style name="Theme.RadRoots" parent="Theme.AppCompat.NoActionBar"> - <item name="windowActionBar">false</item> - <item name="windowNoTitle">true</item> - </style> -</resources> diff --git a/platforms/android/build.gradle.kts b/platforms/android/build.gradle.kts @@ -1,8 +0,0 @@ -plugins { - id("com.android.application") version "8.5.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.24" apply false -} - -tasks.register("clean", Delete::class) { - delete(rootProject.layout.buildDirectory) -} diff --git a/platforms/android/gradle.properties b/platforms/android/gradle.properties @@ -1,3 +0,0 @@ -android.useAndroidX=true -org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -kotlin.code.style=official diff --git a/platforms/android/settings.gradle.kts b/platforms/android/settings.gradle.kts @@ -1,22 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositoriesMode.set(org.gradle.api.initialization.resolve.RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - -rootProject.name = "RadRootsAndroid" - -include(":app") -include(":radrootsAndroidSecurity") - -project(":radrootsAndroidSecurity").projectDir = file("../../native/bridges/android/security/kotlin/RadRootsAndroidSecurity") diff --git a/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h b/platforms/ios/App/Bridge/RadRootsIOS-Bridging-Header.h @@ -1 +0,0 @@ -#include "RadRootsIOSBridge.h" diff --git a/platforms/ios/App/Bridge/RadRootsIOSBridge.h b/platforms/ios/App/Bridge/RadRootsIOSBridge.h @@ -1,5 +0,0 @@ -#include <stdint.h> - -int32_t radroots_ios_run(void); -char *radroots_ios_clipboard_text_copy(void); -void radroots_ios_string_free(char *value); diff --git a/platforms/ios/App/RadRootsIOS.entitlements b/platforms/ios/App/RadRootsIOS.entitlements @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>keychain-access-groups</key> - <array> - <string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string> - </array> -</dict> -</plist> diff --git a/platforms/ios/App/Resources/Info.plist b/platforms/ios/App/Resources/Info.plist @@ -1,74 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>CFBundleDevelopmentRegion</key> - <string>en</string> - <key>CFBundleDisplayName</key> - <string>Rad Roots</string> - <key>CFBundleExecutable</key> - <string>$(EXECUTABLE_NAME)</string> - <key>CFBundleIdentifier</key> - <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> - <key>CFBundleIcons</key> - <dict> - <key>CFBundlePrimaryIcon</key> - <dict> - <key>CFBundleIconFiles</key> - <array> - <string>RadRootsIcon-20@2x</string> - <string>RadRootsIcon-20@3x</string> - <string>RadRootsIcon-29@2x</string> - <string>RadRootsIcon-29@3x</string> - <string>RadRootsIcon-40@2x</string> - <string>RadRootsIcon-40@3x</string> - <string>RadRootsIcon-60@2x</string> - <string>RadRootsIcon-60@3x</string> - </array> - </dict> - </dict> - <key>CFBundleIcons~ipad</key> - <dict> - <key>CFBundlePrimaryIcon</key> - <dict> - <key>CFBundleIconFiles</key> - <array> - <string>RadRootsIcon-20@2x~ipad</string> - <string>RadRootsIcon-29@2x~ipad</string> - <string>RadRootsIcon-40@2x~ipad</string> - <string>RadRootsIcon-76@2x~ipad</string> - <string>RadRootsIcon-83.5@2x~ipad</string> - </array> - </dict> - </dict> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleName</key> - <string>Rad Roots</string> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleShortVersionString</key> - <string>$(MARKETING_VERSION)</string> - <key>CFBundleVersion</key> - <string>$(CURRENT_PROJECT_VERSION)</string> - <key>LSRequiresIPhoneOS</key> - <true/> - <key>UIApplicationSupportsIndirectInputEvents</key> - <true/> - <key>UILaunchStoryboardName</key> - <string>LaunchScreen</string> - <key>UISupportedInterfaceOrientations</key> - <array> - <string>UIInterfaceOrientationPortrait</string> - <string>UIInterfaceOrientationLandscapeLeft</string> - <string>UIInterfaceOrientationLandscapeRight</string> - </array> - <key>UISupportedInterfaceOrientations~ipad</key> - <array> - <string>UIInterfaceOrientationPortrait</string> - <string>UIInterfaceOrientationPortraitUpsideDown</string> - <string>UIInterfaceOrientationLandscapeLeft</string> - <string>UIInterfaceOrientationLandscapeRight</string> - </array> -</dict> -</plist> diff --git a/platforms/ios/App/Resources/LaunchScreen.storyboard b/platforms/ios/App/Resources/LaunchScreen.storyboard @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> - <device id="retina6_12" orientation="portrait" appearance="light"/> - <dependencies> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/> - <capability name="Safe area layout guides" minToolsVersion="9.0"/> - <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> - </dependencies> - <scenes> - <scene sceneID="EHf-IW-A2E"> - <objects> - <viewController id="01J-lp-oVM" sceneMemberID="viewController"> - <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> - <rect key="frame" x="0.0" y="0.0" width="393" height="852"/> - <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <subviews> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Rad Roots" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8c2-Jc-d8M"> - <rect key="frame" x="125.5" y="403" width="142" height="46"/> - <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="28"/> - <color key="textColor" red="0.96078431369999995" green="0.96862745100000002" blue="0.98039215690000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <nil key="highlightedColor"/> - </label> - </subviews> - <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/> - <color key="backgroundColor" red="0.062745098039215685" green="0.074509803921568626" blue="0.10196078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> - <constraints> - <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="1Vb-H9-Hoy"/> - <constraint firstItem="8c2-Jc-d8M" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="rOw-uE-8Jh"/> - </constraints> - </view> - </viewController> - <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> - </objects> - <point key="canvasLocation" x="80" y="84"/> - </scene> - </scenes> -</document> diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-20@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-20@3x.png b/platforms/ios/App/Resources/RadRootsIcon-20@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-29@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-29@3x.png b/platforms/ios/App/Resources/RadRootsIcon-29@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-40@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-40@3x.png b/platforms/ios/App/Resources/RadRootsIcon-40@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@2x.png b/platforms/ios/App/Resources/RadRootsIcon-60@2x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-60@3x.png b/platforms/ios/App/Resources/RadRootsIcon-60@3x.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-76@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png b/platforms/ios/App/Resources/RadRootsIcon-83.5@2x~ipad.png Binary files differ. diff --git a/platforms/ios/App/main.swift b/platforms/ios/App/main.swift @@ -1,23 +0,0 @@ -import Foundation -import UIKit - -@_cdecl("radroots_ios_clipboard_text_copy") -func radroots_ios_clipboard_text_copy() -> UnsafeMutablePointer<CChar>? { - guard let clipboardText = UIPasteboard.general.string? - .trimmingCharacters(in: .whitespacesAndNewlines), - !clipboardText.isEmpty - else { - return nil - } - - return clipboardText.withCString { value in - strdup(value) - } -} - -@_cdecl("radroots_ios_string_free") -func radroots_ios_string_free(_ value: UnsafeMutablePointer<CChar>?) { - free(value) -} - -_ = radroots_ios_run() diff --git a/platforms/ios/Config/Base.xcconfig b/platforms/ios/Config/Base.xcconfig @@ -1,17 +0,0 @@ -PRODUCT_NAME = $(TARGET_NAME) -PRODUCT_MODULE_NAME = RadRootsIOS -GENERATE_INFOPLIST_FILE = NO -IPHONEOS_DEPLOYMENT_TARGET = 17.0 -CODE_SIGN_ENTITLEMENTS = $(SRCROOT)/App/RadRootsIOS.entitlements -SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/App/Bridge/RadRootsIOS-Bridging-Header.h -LIBRARY_SEARCH_PATHS = $(inherited) -LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks -OTHER_LDFLAGS = $(inherited) "$(RUST_LIBRARY_PATH)" -framework Foundation -framework UIKit -framework CoreFoundation -framework Metal -lobjc -liconv -RUST_TARGET_TRIPLE = aarch64-apple-ios-sim -RUST_TARGET_TRIPLE[sdk=iphoneos*] = aarch64-apple-ios -RUST_TARGET_TRIPLE[sdk=iphonesimulator*] = aarch64-apple-ios-sim -RUST_CARGO_PROFILE = debug -RUST_LIBRARY_PATH = $(SRCROOT)/../../target/aarch64-apple-ios-sim/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a -RUST_LIBRARY_PATH[sdk=iphoneos*] = $(SRCROOT)/../../target/aarch64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a -RUST_LIBRARY_PATH[sdk=iphonesimulator*][arch=x86_64] = $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a -SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO diff --git a/platforms/ios/Config/Debug.xcconfig b/platforms/ios/Config/Debug.xcconfig @@ -1,3 +0,0 @@ -#include "Base.xcconfig" - -RUST_CARGO_PROFILE = debug diff --git a/platforms/ios/Config/Release.xcconfig b/platforms/ios/Config/Release.xcconfig @@ -1,3 +0,0 @@ -#include "Base.xcconfig" - -RUST_CARGO_PROFILE = release diff --git a/platforms/ios/Scripts/build_rust_ios.sh b/platforms/ios/Scripts/build_rust_ios.sh @@ -1,89 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -ios_root="$(cd "${script_dir}/.." && pwd -P)" -app_root="$(cd "${ios_root}/../.." && pwd -P)" -ios_target_dir="${app_root}/target" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -require_rust_target() { - local target="$1" - if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then - return - fi - echo "missing required rust target: ${target}" >&2 - exit 1 -} - -profile_for_configuration() { - case "${1}" in - Release) - echo "release" - ;; - *) - echo "debug" - ;; - esac -} - -build_target() { - local target="$1" - local profile="$2" - local cargo_args=( - build - --manifest-path "${app_root}/Cargo.toml" - -p radroots_app_ios - --target "${target}" - ) - if [[ "${profile}" == "release" ]]; then - cargo_args+=(--release) - fi - CARGO_TARGET_DIR="${ios_target_dir}" cargo "${cargo_args[@]}" -} - -build_targets() { - local profile="$1" - shift - for target in "$@"; do - require_rust_target "${target}" - build_target "${target}" "${profile}" - done -} - -require_command cargo -require_command rustup - -configuration="${CONFIGURATION:-Debug}" -profile="$(profile_for_configuration "${configuration}")" -sdk_name="${SDK_NAME:-}" -archs="${ARCHS:-}" - -if [[ -n "${sdk_name}" ]]; then - case "${sdk_name}" in - iphoneos*) - build_targets "${profile}" aarch64-apple-ios - ;; - iphonesimulator*) - if [[ " ${archs} " == *" x86_64 "* ]]; then - build_targets "${profile}" aarch64-apple-ios-sim x86_64-apple-ios - else - build_targets "${profile}" aarch64-apple-ios-sim - fi - ;; - *) - echo "unsupported iOS SDK_NAME: ${sdk_name}" >&2 - exit 1 - ;; - esac - exit 0 -fi - -build_targets "${profile}" aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios diff --git a/platforms/ios/Scripts/sync_geocoder_resource.sh b/platforms/ios/Scripts/sync_geocoder_resource.sh @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "$script_dir/../../.." && pwd -P)" - -source_db="$app_root/assets/geocoder/geonames.db" -source_revision="$app_root/assets/geocoder/geonames.revision" -target_dir="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" -target_db="$target_dir/geonames.db" -target_revision="$target_dir/geonames.revision" - -mkdir -p "$target_dir" - -if [[ -f "$source_db" ]]; then - if [[ ! -f "$source_revision" ]]; then - printf 'stamped ios geocoder revision asset missing at build time: %s\n' "$source_revision" >&2 - exit 1 - fi - cp "$source_db" "$target_db" - cp "$source_revision" "$target_revision" - printf 'synced ios geocoder asset: %s\n' "$target_db" - printf 'synced ios geocoder revision: %s\n' "$target_revision" - exit 0 -fi - -if [[ -f "$target_db" ]]; then - rm -f "$target_db" -fi -if [[ -f "$target_revision" ]]; then - rm -f "$target_revision" -fi - -printf 'ios geocoder asset not present at build time: %s\n' "$source_db" diff --git a/platforms/ios/project.yml b/platforms/ios/project.yml @@ -1,84 +0,0 @@ -name: RadRootsIOS - -options: - createIntermediateGroups: true - deploymentTarget: - iOS: "17.0" - -settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: org.radroots.app.ios - MARKETING_VERSION: 0.1.0 - CURRENT_PROJECT_VERSION: 1 - SWIFT_VERSION: 6.0 - CODE_SIGN_STYLE: Automatic - -configs: - Debug: debug - Release: release - -packages: - RadRootsAppleSecurity: - path: ../../native/bridges/apple/security/swift/RadRootsAppleSecurity - group: Native/Apple/Swift - -targetTemplates: - app_base: - type: application - platform: iOS - deploymentTarget: "17.0" - supportedDestinations: - - iOS - - iOS Simulator - configFiles: - Debug: Config/Debug.xcconfig - Release: Config/Release.xcconfig - sources: - - path: App/main.swift - - path: App/Bridge - buildPhase: headers - - path: App/Resources - excludes: - - Info.plist - settings: - base: - PRODUCT_NAME: $(TARGET_NAME) - INFOPLIST_FILE: App/Resources/Info.plist - TARGETED_DEVICE_FAMILY: 1,2 - preBuildScripts: - - name: Build Rust iOS Library - script: | - "$SRCROOT/Scripts/build_rust_ios.sh" - basedOnDependencyAnalysis: false - inputFiles: - - $(SRCROOT)/../../Cargo.toml - - $(SRCROOT)/../../Cargo.lock - - $(SRCROOT)/../../crates/shared/core/Cargo.toml - - $(SRCROOT)/../../crates/shared/core/src/lib.rs - - $(SRCROOT)/../../crates/shared/remote_signer/Cargo.toml - - $(SRCROOT)/../../crates/shared/remote_signer/src/lib.rs - - $(SRCROOT)/../../crates/bridges/apple/security/Cargo.toml - - $(SRCROOT)/../../crates/bridges/apple/security/src/lib.rs - - $(SRCROOT)/../../crates/launchers/ios/Cargo.toml - - $(SRCROOT)/../../crates/launchers/ios/src/lib.rs - - $(SRCROOT)/Scripts/build_rust_ios.sh - outputFiles: - - $(SRCROOT)/../../target/$(RUST_TARGET_TRIPLE)/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a - - $(SRCROOT)/../../target/x86_64-apple-ios/$(RUST_CARGO_PROFILE)/libradroots_app_ios.a - postBuildScripts: - - name: Sync iOS Geocoder Asset - script: | - "$SRCROOT/Scripts/sync_geocoder_resource.sh" - basedOnDependencyAnalysis: false - dependencies: - - sdk: UIKit.framework - - sdk: Foundation.framework - - sdk: CoreFoundation.framework - - sdk: Metal.framework - - package: RadRootsAppleSecurity - product: RadRootsAppleSecurityFFI - -targets: - RadRootsIOS: - templates: - - app_base diff --git a/scripts/build-android-host.sh b/scripts/build-android-host.sh @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -android_root="${app_root}/platforms/android" -configuration="${CONFIGURATION:-Debug}" - -source "${android_root}/Scripts/android_toolchain_config.sh" - -"${android_root}/Scripts/bootstrap_android_toolchain.sh" - -gradle_task=":app:assembleDebug" -expected_apk="${android_root}/app/build/outputs/apk/debug/app-debug.apk" -if [[ "${configuration}" == "Release" ]]; then - gradle_task=":app:assembleRelease" - expected_apk="${android_root}/app/build/outputs/apk/release/app-release-unsigned.apk" -fi - -( - cd "${android_root}" - GRADLE_USER_HOME="${android_gradle_user_home}" \ - ANDROID_USER_HOME="${android_user_home}" \ - ANDROID_HOME="${android_sdk_dir}" \ - ANDROID_SDK_ROOT="${android_sdk_dir}" \ - "${android_gradle_bin}" --no-daemon "${gradle_task}" -) - -if [[ ! -f "${expected_apk}" ]]; then - echo "missing expected android apk: ${expected_apk}" >&2 - exit 1 -fi - -printf '%s\n' "${expected_apk}" diff --git a/scripts/build-ios-host.sh b/scripts/build-ios-host.sh @@ -1,63 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -ios_root="${app_root}/platforms/ios" -project_name="RadRootsIOS" -configuration="${CONFIGURATION:-Debug}" -derived_data_dir="${ios_root}/.derived-data" -expected_app="${derived_data_dir}/Build/Products/${configuration}-iphonesimulator/${project_name}.app" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -ios_sim_host_arch() { - case "$(uname -m)" in - arm64|aarch64) - echo "arm64" - ;; - x86_64) - echo "x86_64" - ;; - *) - echo "unsupported host architecture for ios simulator: $(uname -m)" >&2 - exit 1 - ;; - esac -} - -require_command xcodegen -require_command xcodebuild - -"${script_dir}/check-ios-target.sh" - -host_arch="$(ios_sim_host_arch)" - -( - cd "${ios_root}" - xcodegen generate - xcodebuild \ - -project "${project_name}.xcodeproj" \ - -scheme "${project_name}" \ - -configuration "${configuration}" \ - -sdk iphonesimulator \ - -destination "generic/platform=iOS Simulator" \ - -derivedDataPath "${derived_data_dir}" \ - ARCHS="${host_arch}" \ - CODE_SIGNING_ALLOWED=YES \ - ONLY_ACTIVE_ARCH=YES \ - build -) - -if [[ ! -d "${expected_app}" ]]; then - echo "missing expected ios app bundle: ${expected_app}" >&2 - exit 1 -fi - -printf '%s\n' "${expected_app}" diff --git a/scripts/check-android-target.sh b/scripts/check-android-target.sh @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -android_root="${app_root}/platforms/android" - -source "${android_root}/Scripts/android_toolchain_config.sh" - -"${android_root}/Scripts/bootstrap_android_toolchain.sh" - -export PATH="${android_cargo_bin_dir}:${PATH}" -export ANDROID_HOME="${android_sdk_dir}" -export ANDROID_SDK_ROOT="${android_sdk_dir}" -export ANDROID_NDK_HOME="${android_ndk_dir}" -export ANDROID_NDK_ROOT="${android_ndk_dir}" -export ANDROID_USER_HOME="${android_user_home}" - -CARGO_TARGET_DIR="${app_root}/target" \ - cargo ndk -t "${android_abi}" check --manifest-path "${app_root}/Cargo.toml" -p radroots_app_android diff --git a/scripts/check-ios-target.sh b/scripts/check-ios-target.sh @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -ios_target_dir="${app_root}/target" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -ios_sim_rust_targets_for_host() { - case "$(uname -m)" in - arm64|aarch64) - printf '%s\n' "aarch64-apple-ios-sim" - ;; - x86_64) - printf '%s\n' "aarch64-apple-ios-sim" "x86_64-apple-ios" - ;; - *) - echo "unsupported host architecture for ios simulator: $(uname -m)" >&2 - exit 1 - ;; - esac -} - -require_rust_target() { - local target="$1" - if rustup target list --installed | grep -Fx "${target}" >/dev/null 2>&1; then - return - fi - echo "missing required rust target: ${target}" >&2 - exit 1 -} - -require_command cargo -require_command rustup - -cd "${app_root}" - -declare -a targets=() -if [[ -n "${IOS_SIM_RUST_TARGET:-}" ]]; then - targets=("${IOS_SIM_RUST_TARGET}") -else - while IFS= read -r target; do - targets+=("${target}") - done < <(ios_sim_rust_targets_for_host) -fi - -for target in "${targets[@]}"; do - require_rust_target "${target}" - CARGO_TARGET_DIR="${ios_target_dir}" \ - cargo check --manifest-path "${app_root}/Cargo.toml" -p radroots_app_ios --target "${target}" -done diff --git a/scripts/check.sh b/scripts/check.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)" + +cd "${repo_root}" +cargo check -p radroots_app diff --git a/scripts/run-android-emulator.sh b/scripts/run-android-emulator.sh @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -android_root="${app_root}/platforms/android" -bundle_id="org.radroots.app.android" -activity_name="${bundle_id}/.MainActivity" - -source "${android_root}/Scripts/android_toolchain_config.sh" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -host_os() { - uname -s -} - -host_arch() { - uname -m -} - -android_emulator_gpu_mode() { - if [[ -n "${RADROOTS_ANDROID_EMULATOR_GPU_MODE:-}" ]]; then - printf '%s\n' "${RADROOTS_ANDROID_EMULATOR_GPU_MODE}" - return - fi - - if [[ "$(host_os)" == "Darwin" && "$(host_arch)" == "arm64" ]]; then - printf '%s\n' "swiftshader" - return - fi - - printf '%s\n' "auto" -} - -running_emulator_serial() { - local target_avd="$1" - while read -r serial state _; do - [[ "${serial}" == emulator-* ]] || continue - [[ "${state}" == "device" || "${state}" == "offline" ]] || continue - if [[ "$("${android_adb_bin}" -s "${serial}" emu avd name 2>/dev/null | sed -n '1p' | tr -d '\r')" == "${target_avd}" ]]; then - printf '%s\n' "${serial}" - return - fi - done < <("${android_adb_bin}" devices | tail -n +2) -} - -ensure_avd() { - local avd_name="$1" - if [[ -d "${android_avd_home}/${avd_name}.avd" ]]; then - return - fi - - mkdir -p "${android_avd_home}" "${android_emulator_home}" - printf 'no\n' | \ - ANDROID_AVD_HOME="${android_avd_home}" \ - ANDROID_EMULATOR_HOME="${android_emulator_home}" \ - "${android_avdmanager_bin}" create avd --force --name "${avd_name}" --package "$(android_emulator_system_image_package)" -} - -wait_for_boot_complete() { - local serial="$1" - "${android_adb_bin}" -s "${serial}" wait-for-device >/dev/null - until [[ "$("${android_adb_bin}" -s "${serial}" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" == "1" ]]; do - sleep 2 - done -} - -launch_emulator_if_needed() { - local avd_name="$1" - local serial - local gpu_mode - serial="$(running_emulator_serial "${avd_name}" || true)" - if [[ -n "${serial}" ]]; then - printf '%s\n' "${serial}" - return - fi - - gpu_mode="$(android_emulator_gpu_mode)" - ANDROID_AVD_HOME="${android_avd_home}" \ - ANDROID_EMULATOR_HOME="${android_emulator_home}" \ - nohup "${android_emulator_bin}" \ - -avd "${avd_name}" \ - -gpu "${gpu_mode}" \ - -no-snapshot-load \ - -no-snapshot-save >/tmp/radroots-android-emulator.log 2>&1 & - - for _ in $(seq 1 60); do - serial="$(running_emulator_serial "${avd_name}" || true)" - if [[ -n "${serial}" ]]; then - printf '%s\n' "${serial}" - return - fi - sleep 2 - done - - echo "android emulator failed to start" >&2 - exit 1 -} - -require_command mktemp - -avd_name="${1:-$(android_avd_name)}" - -"${android_root}/Scripts/bootstrap_android_toolchain.sh" --with-emulator - -build_log="$(mktemp)" -trap 'rm -f "${build_log}"' EXIT - -if ! "${script_dir}/build-android-host.sh" | tee "${build_log}"; then - exit 1 -fi - -apk_path="$(tail -n 1 "${build_log}")" -if [[ ! -f "${apk_path}" ]]; then - echo "missing built android apk: ${apk_path}" >&2 - exit 1 -fi - -ensure_avd "${avd_name}" -serial="$(launch_emulator_if_needed "${avd_name}")" -wait_for_boot_complete "${serial}" - -"${android_adb_bin}" -s "${serial}" install -r "${apk_path}" -"${android_adb_bin}" -s "${serial}" shell am start -n "${activity_name}" diff --git a/scripts/run-ios-simulator.sh b/scripts/run-ios-simulator.sh @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -app_root="$(cd "${script_dir}/.." && pwd -P)" -bundle_id="org.radroots.app.ios" -device_selector="${1:-${IOS_SIMULATOR_DEVICE:-iPhone 16}}" - -require_command() { - if command -v "$1" >/dev/null 2>&1; then - return - fi - echo "missing required command: $1" >&2 - exit 1 -} - -resolve_simulator_udid() { - local selector="$1" - if [[ "${selector}" =~ ^[0-9A-F-]{36}$ ]]; then - printf '%s\n' "${selector}" - return - fi - - local line - line="$( - xcrun simctl list devices available | - awk -v name="${selector}" '$0 ~ ("^[[:space:]]+" name " \\(") { print; exit }' - )" - - if [[ -z "${line}" ]]; then - echo "unable to find available iOS simulator: ${selector}" >&2 - exit 1 - fi - - printf '%s\n' "${line}" | awk -F '[()]' '{ print $2 }' -} - -require_command open -require_command xcrun -require_command mktemp - -build_log="$(mktemp)" -trap 'rm -f "${build_log}"' EXIT - -if ! "${script_dir}/build-ios-host.sh" | tee "${build_log}"; then - exit 1 -fi - -app_path="$(tail -n 1 "${build_log}")" -if [[ ! -d "${app_path}" ]]; then - echo "missing built iOS app bundle: ${app_path}" >&2 - exit 1 -fi - -device_udid="$(resolve_simulator_udid "${device_selector}")" - -xcrun simctl boot "${device_udid}" >/dev/null 2>&1 || true -xcrun simctl bootstatus "${device_udid}" -b -open -a Simulator --args -CurrentDeviceUDID "${device_udid}" >/dev/null 2>&1 || open -a Simulator >/dev/null 2>&1 -xcrun simctl install "${device_udid}" "${app_path}" -xcrun simctl launch "${device_udid}" "${bundle_id}" diff --git a/scripts/run.sh b/scripts/run.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)" + +cd "${repo_root}" +cargo run -p radroots_app -- "$@" diff --git a/scripts/verify-approved-test-fixtures.sh b/scripts/verify-approved-test-fixtures.sh @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$repo_root" - -if rg -n \ - --glob '*.rs' \ - --glob '*.kt' \ - --glob '*.swift' \ - --glob '*.sh' \ - --glob '!scripts/verify-approved-test-fixtures.sh' \ - 'npub1abc|nsec1example' \ - crates native platforms scripts; then - echo "found banned placeholder fixture literals" >&2 - exit 1 -fi diff --git a/scripts/with-wasm-toolchain.sh b/scripts/with-wasm-toolchain.sh @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ "$#" -eq 0 ]; then - echo "usage: $0 <command> [args...]" >&2 - exit 64 -fi - -unset NO_COLOR - -probe_wasm_clang() { - local clang_bin="$1" - local probe_file - - if [ ! -x "$clang_bin" ]; then - return 1 - fi - - probe_file="$(mktemp)" - trap 'rm -f "$probe_file"' RETURN - printf 'int main(void){return 0;}\n' \ - | "$clang_bin" --target=wasm32-unknown-unknown -x c -c - -o "$probe_file" >/dev/null 2>&1 -} - -if [ -z "${CC_wasm32_unknown_unknown:-}" ]; then - if probe_wasm_clang /opt/homebrew/opt/llvm/bin/clang; then - export PATH="/opt/homebrew/opt/llvm/bin:$PATH" - export CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang - elif command -v clang >/dev/null 2>&1 && probe_wasm_clang "$(command -v clang)"; then - export CC_wasm32_unknown_unknown - CC_wasm32_unknown_unknown="$(command -v clang)" - else - echo "no wasm-capable clang found; install llvm or set CC_wasm32_unknown_unknown" >&2 - exit 1 - fi -fi - -exec "$@" diff --git a/src/app.rs b/src/app.rs @@ -0,0 +1,40 @@ +use gpui::{AppContext, Application, WindowOptions, px, size}; + +fn titlebar_options() -> gpui::TitlebarOptions { + gpui::TitlebarOptions { + title: None, + appears_transparent: true, + ..Default::default() + } +} + +pub fn launch() { + let app = Application::new(); + + app.run(|cx| { + cx.on_window_closed(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + cx.spawn(async move |cx| { + cx.open_window( + WindowOptions { + app_id: Some("org.radroots.app".to_owned()), + window_min_size: Some(size(px(640.0), px(480.0))), + titlebar: Some(titlebar_options()), + ..Default::default() + }, + |_, cx| cx.new(|_| crate::window::PlaceholderView), + ) + .expect("main radroots app window should open"); + + cx.update(|cx| cx.activate(true)) + .expect("radroots app activation should succeed"); + }) + .detach(); + }); + +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,8 @@ +#![forbid(unsafe_code)] + +mod app; +mod window; + +pub fn run() { + app::launch(); +} diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,5 @@ +#![forbid(unsafe_code)] + +fn main() { + radroots_app::run(); +} diff --git a/src/window.rs b/src/window.rs @@ -0,0 +1,21 @@ +use gpui::{Context, FontWeight, IntoElement, ParentElement, Render, Styled, Window, div, px, rgb}; + +pub struct PlaceholderView; + +impl Render for PlaceholderView { + fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .bg(rgb(0xF5F1E8)) + .child( + div() + .text_size(px(20.0)) + .font_weight(FontWeight::SEMIBOLD) + .text_color(rgb(0x1F2C23)) + .child("radroots"), + ) + } +}