commit bd0ced242d453eacb44a352f09e9fc0bb5b5e121
parent 1dad49751d9ea58bae97c6f33dc783f5ee6078ea
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 03:10:37 +0000
app: add host-vault accounts bootstrap
Diffstat:
7 files changed, 1263 insertions(+), 27 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -9,6 +9,16 @@ 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"
@@ -61,6 +71,12 @@ dependencies = [
]
[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -111,6 +127,12 @@ dependencies = [
]
[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -496,6 +518,12 @@ checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b"
[[package]]
name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
@@ -507,6 +535,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
+name = "bech32"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f"
+
+[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -527,6 +561,17 @@ dependencies = [
]
[[package]]
+name = "bip39"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
+dependencies = [
+ "bitcoin_hashes",
+ "serde",
+ "unicode-normalization",
+]
+
+[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -548,6 +593,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
+name = "bitcoin-io"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
+dependencies = [
+ "bitcoin-io",
+ "hex-conservative",
+ "serde",
+]
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -558,6 +620,9 @@ name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+dependencies = [
+ "serde_core",
+]
[[package]]
name = "bitstream-io"
@@ -842,6 +907,30 @@ dependencies = [
]
[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -888,7 +977,7 @@ dependencies = [
"cocoa-foundation 0.1.2",
"core-foundation 0.9.4",
"core-graphics 0.23.2",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
"objc",
]
@@ -904,7 +993,7 @@ dependencies = [
"cocoa-foundation 0.2.0",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
"objc",
]
@@ -992,6 +1081,25 @@ dependencies = [
]
[[package]]
+name = "config"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+dependencies = [
+ "async-trait",
+ "convert_case 0.6.0",
+ "json5",
+ "nom 7.1.3",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml 0.8.23",
+ "yaml-rust2",
+]
+
+[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1030,6 +1138,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1064,7 +1181,7 @@ dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -1077,7 +1194,7 @@ dependencies = [
"bitflags 2.11.1",
"core-foundation 0.10.0",
"core-graphics-types 0.2.0",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -1090,7 +1207,7 @@ dependencies = [
"bitflags 2.11.1",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -1137,7 +1254,7 @@ checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130"
dependencies = [
"core-foundation 0.10.0",
"core-graphics 0.24.0",
- "foreign-types",
+ "foreign-types 0.5.0",
"libc",
]
@@ -1324,6 +1441,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
+name = "dbus"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "dbus-secret-service"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
+dependencies = [
+ "dbus",
+ "openssl",
+ "zeroize",
+]
+
+[[package]]
name = "deflate64"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1354,7 +1493,7 @@ version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
- "convert_case",
+ "convert_case 0.4.0",
"proc-macro2",
"quote",
"rustc_version",
@@ -1450,6 +1589,15 @@ dependencies = [
]
[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1901,12 +2049,21 @@ dependencies = [
[[package]]
name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.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",
+ "foreign-types-shared 0.3.1",
]
[[package]]
@@ -1922,6 +2079,12 @@ dependencies = [
[[package]]
name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
@@ -2268,7 +2431,7 @@ dependencies = [
"etagere",
"filedescriptor",
"flume",
- "foreign-types",
+ "foreign-types 0.5.0",
"futures",
"gpui-macros",
"gpui_collections",
@@ -2463,7 +2626,7 @@ dependencies = [
"core-foundation 0.10.0",
"core-video",
"ctor",
- "foreign-types",
+ "foreign-types 0.5.0",
"metal",
"objc",
]
@@ -2600,6 +2763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
+ "allocator-api2",
]
[[package]]
@@ -2619,6 +2783,15 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
@@ -2651,6 +2824,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3048,6 +3230,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
@@ -3172,6 +3357,34 @@ dependencies = [
]
[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "keyring"
+version = "3.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
+dependencies = [
+ "byteorder",
+ "dbus-secret-service",
+ "linux-keyutils",
+ "log",
+ "openssl",
+ "security-framework 2.11.1",
+ "security-framework 3.7.0",
+ "windows-sys 0.60.2",
+ "zeroize",
+]
+
+[[package]]
name = "khronos-egl"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3264,6 +3477,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
name = "libfuzzer-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3313,6 +3536,16 @@ dependencies = [
]
[[package]]
+name = "linux-keyutils"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590"
+dependencies = [
+ "bitflags 2.11.1",
+ "libc",
+]
+
+[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3541,7 +3774,7 @@ dependencies = [
"bitflags 2.11.1",
"block",
"core-graphics-types 0.1.3",
- "foreign-types",
+ "foreign-types 0.5.0",
"log",
"objc",
"paste",
@@ -3784,6 +4017,31 @@ dependencies = [
]
[[package]]
+name = "nostr"
+version = "0.44.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
+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",
+]
+
+[[package]]
name = "notify"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4135,6 +4393,12 @@ dependencies = [
]
[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
name = "open"
version = "5.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4146,18 +4410,76 @@ dependencies = [
]
[[package]]
+name = "openssl"
+version = "0.10.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
+name = "openssl-src"
+version = "300.6.0+3.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644"
+dependencies = [
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4197,6 +4519,17 @@ dependencies = [
]
[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4250,6 +4583,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
+name = "pest"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4405,6 +4781,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
name = "postage"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4665,6 +5052,8 @@ dependencies = [
"radroots_app_state",
"radroots_app_sync",
"radroots_app_ui",
+ "radroots_nostr_accounts",
+ "radroots_secret_vault",
"thiserror 2.0.18",
"tracing",
"tracing-subscriber",
@@ -4741,6 +5130,153 @@ dependencies = [
]
[[package]]
+name = "radroots_core"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "rust_decimal",
+ "rust_decimal_macros",
+ "serde",
+]
+
+[[package]]
+name = "radroots_events"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "radroots_core",
+ "serde",
+]
+
+[[package]]
+name = "radroots_identity"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "nostr",
+ "radroots_events",
+ "radroots_protected_store",
+ "radroots_runtime",
+ "radroots_runtime_paths",
+ "radroots_secret_vault",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tracing",
+]
+
+[[package]]
+name = "radroots_log"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "chrono",
+ "thiserror 1.0.69",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "radroots_nostr"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "nostr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "radroots_nostr_accounts"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "radroots_identity",
+ "radroots_nostr_signer",
+ "radroots_protected_store",
+ "radroots_runtime",
+ "radroots_secret_vault",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "zeroize",
+]
+
+[[package]]
+name = "radroots_nostr_connect"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "nostr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "url",
+]
+
+[[package]]
+name = "radroots_nostr_signer"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "hex",
+ "nostr",
+ "radroots_identity",
+ "radroots_nostr",
+ "radroots_nostr_connect",
+ "radroots_runtime",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 1.0.69",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "radroots_protected_store"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "chacha20poly1305",
+ "getrandom 0.2.17",
+ "radroots_secret_vault",
+ "serde",
+ "serde_json",
+ "zeroize",
+]
+
+[[package]]
+name = "radroots_runtime"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "anyhow",
+ "chacha20poly1305",
+ "config",
+ "getrandom 0.2.17",
+ "radroots_log",
+ "radroots_protected_store",
+ "radroots_runtime_paths",
+ "radroots_secret_vault",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "toml 0.8.23",
+ "tracing",
+ "zeroize",
+]
+
+[[package]]
+name = "radroots_runtime_paths"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "serde",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "radroots_secret_vault"
+version = "0.1.0-alpha.2"
+dependencies = [
+ "keyring",
+]
+
+[[package]]
name = "rand"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5028,6 +5564,18 @@ dependencies = [
]
[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags 2.11.1",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
name = "ropey"
version = "2.0.0-beta.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5051,7 +5599,7 @@ dependencies = [
"bitflags 2.11.1",
"fallible-iterator",
"fallible-streaming-iterator",
- "hashlink",
+ "hashlink 0.9.1",
"libsqlite3-sys",
"smallvec",
]
@@ -5147,6 +5695,38 @@ dependencies = [
]
[[package]]
+name = "rust-ini"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
+dependencies = [
+ "arrayvec",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "rust_decimal_macros"
+version = "1.40.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5216,7 +5796,7 @@ dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
- "security-framework",
+ "security-framework 3.7.0",
]
[[package]]
@@ -5297,6 +5877,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5376,12 +5965,57 @@ dependencies = [
]
[[package]]
+name = "scrypt"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+dependencies = [
+ "password-hash",
+ "pbkdf2",
+ "salsa20",
+ "sha2",
+]
+
+[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
+name = "secp256k1"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
+dependencies = [
+ "rand 0.8.6",
+ "secp256k1-sys",
+ "serde",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.11.1",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6295,11 +6929,24 @@ dependencies = [
"libc",
"mio",
"pin-project-lite",
+ "signal-hook-registry",
"socket2",
+ "tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
name = "tokio-rustls"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6617,6 +7264,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6700,6 +7353,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6736,6 +7398,16 @@ 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 = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6766,7 +7438,7 @@ version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
dependencies = [
- "base64",
+ "base64 0.22.1",
"data-url",
"flate2",
"fontdb 0.23.0",
@@ -6955,6 +7627,7 @@ dependencies = [
"cfg-if",
"once_cell",
"rustversion",
+ "serde",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
@@ -7929,6 +8602,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink 0.8.4",
+]
+
+[[package]]
name = "yazi"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8074,7 +8758,7 @@ version = "0.12.15-zed"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
diff --git a/Cargo.toml b/Cargo.toml
@@ -30,6 +30,8 @@ hex = "0.4"
mf2-i18n-build = { git = "https://github.com/triesap/mf2-i18n.git", rev = "0c3ba2729b309f27aed3e27ae4e753b0147a75ec" }
mf2-i18n-core = { git = "https://github.com/triesap/mf2-i18n.git", rev = "0c3ba2729b309f27aed3e27ae4e753b0147a75ec" }
mf2-i18n-native = { git = "https://github.com/triesap/mf2-i18n.git", rev = "0c3ba2729b309f27aed3e27ae4e753b0147a75ec" }
+radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", features = ["os-keyring"] }
+radroots_secret_vault = { path = "../lib/crates/secret_vault", default-features = false, features = ["std", "os-keyring"] }
radroots_app_core = { path = "crates/shared/core", version = "0.1.0" }
radroots_app_i18n = { path = "crates/shared/i18n", version = "0.1.0" }
radroots_app_models = { path = "crates/shared/models", version = "0.1.0" }
diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml
@@ -11,6 +11,8 @@ publish = false
gpui.workspace = true
gpui-component.workspace = true
gpui-component-assets.workspace = true
+radroots_nostr_accounts.workspace = true
+radroots_secret_vault.workspace = true
radroots_app_core.workspace = true
radroots_app_i18n.workspace = true
radroots_app_models.workspace = true
diff --git a/crates/launchers/desktop/src/accounts.rs b/crates/launchers/desktop/src/accounts.rs
@@ -0,0 +1,470 @@
+use std::{env, fs, path::PathBuf};
+
+use radroots_app_core::AppSharedAccountsPaths;
+use radroots_app_models::{
+ AccountSummary, AppIdentityProjection, FarmerActivationProjection, IdentityBlockedReason,
+ SelectedAccountProjection, SelectedSurfaceProjection,
+};
+use radroots_app_sqlite::{AppSqliteError, AppSqliteStore};
+use radroots_nostr_accounts::prelude::{
+ RadrootsNostrAccountRecord, RadrootsNostrAccountStore, RadrootsNostrAccountStoreState,
+ RadrootsNostrAccountsError, RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
+ RadrootsNostrSelectedAccountStatus,
+};
+use radroots_secret_vault::{
+ RadrootsHostVaultCapabilities, RadrootsHostVaultPolicy, RadrootsSecretBackend,
+ RadrootsSecretBackendAvailability, RadrootsSecretBackendSelection, RadrootsSecretVault,
+ RadrootsSecretVaultError, RadrootsSecretVaultOsKeyring,
+};
+use thiserror::Error;
+
+const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_APP_HOST_VAULT_AVAILABLE";
+const HOST_VAULT_SERVICE_NAME: &str = "org.radroots.app.local-account";
+const HOST_VAULT_PROBE_SLOT: &str = "__radroots_app_host_vault_probe__";
+
+pub struct DesktopAccountsBootstrap {
+ pub accounts_manager: Option<RadrootsNostrAccountsManager>,
+ pub identity_projection: AppIdentityProjection,
+}
+
+pub fn bootstrap_desktop_accounts(
+ paths: &AppSharedAccountsPaths,
+ sqlite_store: &AppSqliteStore,
+) -> Result<DesktopAccountsBootstrap, DesktopAccountsBootstrapError> {
+ bootstrap_desktop_accounts_with_availability(
+ paths,
+ sqlite_store,
+ secret_backend_availability()?,
+ )
+}
+
+fn bootstrap_desktop_accounts_with_availability(
+ paths: &AppSharedAccountsPaths,
+ sqlite_store: &AppSqliteStore,
+ availability: RadrootsSecretBackendAvailability,
+) -> Result<DesktopAccountsBootstrap, DesktopAccountsBootstrapError> {
+ ensure_directory(paths.data_root.as_path())?;
+ ensure_directory(paths.secrets_root.as_path())?;
+
+ let selection = local_account_secret_backend_selection();
+ let store = RadrootsNostrFileAccountStore::new(paths.store_path.as_path());
+
+ match RadrootsNostrAccountsManager::resolve_local_backend(selection, availability) {
+ Ok(_) => {
+ let (accounts_manager, _) = RadrootsNostrAccountsManager::new_local_file_backed(
+ paths.store_path.as_path(),
+ paths.secrets_root.as_path(),
+ selection,
+ availability,
+ HOST_VAULT_SERVICE_NAME,
+ )?;
+ let identity_projection =
+ identity_projection_from_manager(&accounts_manager, sqlite_store)?;
+
+ Ok(DesktopAccountsBootstrap {
+ accounts_manager: Some(accounts_manager),
+ identity_projection,
+ })
+ }
+ Err(RadrootsSecretVaultError::BackendUnavailable { .. })
+ | Err(RadrootsSecretVaultError::FallbackUnavailable { .. }) => {
+ let state = store.load()?;
+ let identity_projection =
+ blocked_identity_projection_from_store_state(state, sqlite_store)?;
+
+ Ok(DesktopAccountsBootstrap {
+ accounts_manager: None,
+ identity_projection,
+ })
+ }
+ Err(error) => Err(error.into()),
+ }
+}
+
+fn ensure_directory(path: &std::path::Path) -> Result<(), DesktopAccountsBootstrapError> {
+ fs::create_dir_all(path).map_err(|source| DesktopAccountsBootstrapError::CreateDirectory {
+ path: path.to_path_buf(),
+ source,
+ })
+}
+
+fn local_account_secret_backend_selection() -> RadrootsSecretBackendSelection {
+ RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ fallback: None,
+ }
+}
+
+fn secret_backend_availability()
+-> Result<RadrootsSecretBackendAvailability, DesktopAccountsBootstrapError> {
+ Ok(RadrootsSecretBackendAvailability {
+ host_vault: host_vault_capabilities()?,
+ encrypted_file: false,
+ external_command: false,
+ memory: false,
+ })
+}
+
+fn host_vault_capabilities() -> Result<RadrootsHostVaultCapabilities, DesktopAccountsBootstrapError>
+{
+ if let Some(available) = host_vault_availability_override()? {
+ return Ok(match available {
+ true => RadrootsHostVaultCapabilities::desktop_keyring(),
+ false => RadrootsHostVaultCapabilities::unavailable(),
+ });
+ }
+
+ let keyring = RadrootsSecretVaultOsKeyring::new(HOST_VAULT_SERVICE_NAME);
+ match keyring.load_secret(HOST_VAULT_PROBE_SLOT) {
+ Ok(_) => Ok(RadrootsHostVaultCapabilities::desktop_keyring()),
+ Err(_) => Ok(RadrootsHostVaultCapabilities::unavailable()),
+ }
+}
+
+fn host_vault_availability_override() -> Result<Option<bool>, DesktopAccountsBootstrapError> {
+ let Ok(value) = env::var(HOST_VAULT_AVAILABILITY_OVERRIDE_ENV) else {
+ return Ok(None);
+ };
+
+ parse_bool_value(HOST_VAULT_AVAILABILITY_OVERRIDE_ENV, value.trim()).map(Some)
+}
+
+fn parse_bool_value(key: &str, value: &str) -> Result<bool, DesktopAccountsBootstrapError> {
+ match value.trim().to_ascii_lowercase().as_str() {
+ "1" | "true" | "yes" | "on" => Ok(true),
+ "0" | "false" | "no" | "off" => Ok(false),
+ other => Err(DesktopAccountsBootstrapError::Configuration(format!(
+ "{key} must be a boolean value, got `{other}`"
+ ))),
+ }
+}
+
+fn blocked_identity_projection_from_store_state(
+ state: RadrootsNostrAccountStoreState,
+ sqlite_store: &AppSqliteStore,
+) -> Result<AppIdentityProjection, DesktopAccountsBootstrapError> {
+ let selected_account = selected_account_from_store_state(&state, sqlite_store)?;
+
+ Ok(AppIdentityProjection::blocked_with_selection(
+ IdentityBlockedReason::HostVaultUnavailable,
+ account_roster_from_records(state.accounts.as_slice()),
+ selected_account,
+ ))
+}
+
+fn identity_projection_from_manager(
+ manager: &RadrootsNostrAccountsManager,
+ sqlite_store: &AppSqliteStore,
+) -> Result<AppIdentityProjection, DesktopAccountsBootstrapError> {
+ let roster_records = manager.list_accounts()?;
+ let roster = account_roster_from_records(roster_records.as_slice());
+
+ match manager.selected_account_status()? {
+ RadrootsNostrSelectedAccountStatus::NotConfigured => {
+ Ok(AppIdentityProjection::missing_with_roster(roster))
+ }
+ RadrootsNostrSelectedAccountStatus::PublicOnly { account }
+ | RadrootsNostrSelectedAccountStatus::Ready { account } => {
+ Ok(AppIdentityProjection::ready(
+ roster,
+ selected_account_projection_from_record(&account, sqlite_store)?,
+ ))
+ }
+ }
+}
+
+fn selected_account_from_store_state(
+ state: &RadrootsNostrAccountStoreState,
+ sqlite_store: &AppSqliteStore,
+) -> Result<Option<SelectedAccountProjection>, DesktopAccountsBootstrapError> {
+ let Some(selected_account_id) = state.selected_account_id.as_ref() else {
+ return Ok(None);
+ };
+ let Some(record) = state
+ .accounts
+ .iter()
+ .find(|record| &record.account_id == selected_account_id)
+ else {
+ return Ok(None);
+ };
+
+ selected_account_projection_from_record(record, sqlite_store).map(Some)
+}
+
+fn selected_account_projection_from_record(
+ record: &RadrootsNostrAccountRecord,
+ sqlite_store: &AppSqliteStore,
+) -> Result<SelectedAccountProjection, DesktopAccountsBootstrapError> {
+ let account = account_summary_from_record(record);
+
+ Ok(
+ match sqlite_store.load_surface_activation(account.account_id.as_str())? {
+ Some(activation) => {
+ SelectedAccountProjection::from_surface_activation(account, activation)
+ }
+ None => SelectedAccountProjection::new(
+ account,
+ SelectedSurfaceProjection::default(),
+ FarmerActivationProjection::inactive(),
+ ),
+ },
+ )
+}
+
+fn account_roster_from_records(records: &[RadrootsNostrAccountRecord]) -> Vec<AccountSummary> {
+ records.iter().map(account_summary_from_record).collect()
+}
+
+fn account_summary_from_record(record: &RadrootsNostrAccountRecord) -> AccountSummary {
+ AccountSummary {
+ account_id: record.account_id.to_string(),
+ npub: record.public_identity.public_key_npub.clone(),
+ label: record.label.clone(),
+ custody: radroots_app_models::AccountCustody::LocalManaged,
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum DesktopAccountsBootstrapError {
+ #[error("failed to create runtime directory {path}: {source}")]
+ CreateDirectory {
+ path: PathBuf,
+ source: std::io::Error,
+ },
+ #[error(transparent)]
+ Sqlite(#[from] AppSqliteError),
+ #[error(transparent)]
+ Accounts(#[from] RadrootsNostrAccountsError),
+ #[error(transparent)]
+ SecretVault(#[from] RadrootsSecretVaultError),
+ #[error("{0}")]
+ Configuration(String),
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ fs,
+ path::PathBuf,
+ sync::Arc,
+ time::{SystemTime, UNIX_EPOCH},
+ };
+
+ use radroots_app_core::AppSharedAccountsPaths;
+ use radroots_app_models::{
+ ActiveSurface, AppStartupGate, IdentityBlockedReason, IdentityReadiness,
+ SelectedSurfaceProjection,
+ };
+ use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget};
+ use radroots_nostr_accounts::prelude::{
+ RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
+ RadrootsNostrMemoryAccountStore, RadrootsNostrSecretVaultMemory,
+ };
+ use radroots_secret_vault::RadrootsHostVaultCapabilities;
+
+ use super::{
+ account_summary_from_record, blocked_identity_projection_from_store_state,
+ bootstrap_desktop_accounts_with_availability, identity_projection_from_manager,
+ selected_account_projection_from_record,
+ };
+
+ fn temp_shared_accounts_paths(label: &str) -> AppSharedAccountsPaths {
+ let suffix = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("clock")
+ .as_nanos();
+ let base = std::env::temp_dir().join(format!("radroots_app_accounts_{label}_{suffix}"));
+
+ AppSharedAccountsPaths {
+ data_root: base.join("data/shared/accounts"),
+ secrets_root: base.join("secrets/shared/accounts"),
+ store_path: base.join("data/shared/accounts/store.json"),
+ }
+ }
+
+ fn unavailable_secret_backend_availability()
+ -> radroots_secret_vault::RadrootsSecretBackendAvailability {
+ radroots_secret_vault::RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: false,
+ external_command: false,
+ memory: false,
+ }
+ }
+
+ #[test]
+ fn blocked_bootstrap_keeps_roster_and_selected_account_when_host_vault_is_unavailable() {
+ let paths = temp_shared_accounts_paths("blocked");
+ fs::create_dir_all(paths.data_root.as_path()).expect("data root should create");
+ fs::create_dir_all(paths.secrets_root.as_path()).expect("secrets root should create");
+ let store = Arc::new(RadrootsNostrFileAccountStore::new(
+ paths.store_path.as_path(),
+ ));
+ let manager = RadrootsNostrAccountsManager::new(
+ store,
+ Arc::new(RadrootsNostrSecretVaultMemory::new()),
+ )
+ .expect("file-backed memory manager should build");
+ let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store");
+
+ let first_account_id = manager
+ .generate_identity(Some("North field".to_owned()), true)
+ .expect("first account should generate");
+ let second_account_id = manager
+ .generate_identity(Some("South field".to_owned()), false)
+ .expect("second account should generate");
+ manager
+ .select_account(&first_account_id)
+ .expect("first account should remain selected");
+
+ let bootstrap = bootstrap_desktop_accounts_with_availability(
+ &paths,
+ &sqlite_store,
+ unavailable_secret_backend_availability(),
+ )
+ .expect("blocked bootstrap should succeed");
+
+ assert!(bootstrap.accounts_manager.is_none());
+ assert_eq!(
+ bootstrap.identity_projection.readiness,
+ IdentityReadiness::Blocked(IdentityBlockedReason::HostVaultUnavailable)
+ );
+ assert_eq!(
+ bootstrap.identity_projection.startup_gate(),
+ AppStartupGate::Blocked
+ );
+ assert_eq!(bootstrap.identity_projection.roster.len(), 2);
+ assert_eq!(
+ bootstrap
+ .identity_projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.account.account_id.as_str()),
+ Some(first_account_id.as_str())
+ );
+ assert!(
+ bootstrap
+ .identity_projection
+ .roster
+ .iter()
+ .any(|account| account.account_id == second_account_id.as_str())
+ );
+
+ cleanup_paths(&paths);
+ }
+
+ #[test]
+ fn manager_projection_uses_selected_account_and_activation_state() {
+ let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store");
+ let manager = RadrootsNostrAccountsManager::new(
+ Arc::new(RadrootsNostrMemoryAccountStore::new()),
+ Arc::new(RadrootsNostrSecretVaultMemory::new()),
+ )
+ .expect("memory manager should build");
+ let account_id = manager
+ .generate_identity(Some("North field".to_owned()), true)
+ .expect("account should generate");
+ let selected_account = manager
+ .selected_account()
+ .expect("selected account should load")
+ .expect("selected account should exist");
+ let selected_account_summary = account_summary_from_record(&selected_account);
+ let selected_account_projection =
+ selected_account_projection_from_record(&selected_account, &sqlite_store)
+ .expect("selected account projection");
+
+ assert_eq!(
+ selected_account_projection.account,
+ selected_account_summary
+ );
+ assert_eq!(
+ selected_account_projection.selected_surface,
+ SelectedSurfaceProjection::default()
+ );
+
+ let activation = radroots_app_models::AccountSurfaceActivationProjection::new(
+ account_id.as_str(),
+ SelectedSurfaceProjection::new(ActiveSurface::Farmer),
+ radroots_app_models::FarmerActivationProjection::active(
+ radroots_app_models::FarmId::new(),
+ ),
+ );
+ sqlite_store
+ .save_surface_activation(&activation)
+ .expect("surface activation should save");
+
+ let projection =
+ identity_projection_from_manager(&manager, &sqlite_store).expect("projection");
+
+ assert_eq!(projection.readiness, IdentityReadiness::Ready);
+ assert_eq!(projection.startup_gate(), AppStartupGate::Farmer);
+ assert_eq!(projection.roster.len(), 1);
+ assert_eq!(
+ projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.account.account_id.as_str()),
+ Some(account_id.as_str())
+ );
+ assert_eq!(
+ projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.active_surface()),
+ Some(ActiveSurface::Farmer)
+ );
+ assert!(
+ projection
+ .selected_account
+ .as_ref()
+ .is_some_and(|account| account.farmer_activation.is_active())
+ );
+ }
+
+ #[test]
+ fn blocked_projection_from_store_state_ignores_stale_selected_account_ids() {
+ let sqlite_store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("sqlite store");
+ let manager = RadrootsNostrAccountsManager::new(
+ Arc::new(RadrootsNostrMemoryAccountStore::new()),
+ Arc::new(RadrootsNostrSecretVaultMemory::new()),
+ )
+ .expect("memory manager should build");
+ let account_id = manager
+ .generate_identity(Some("North field".to_owned()), true)
+ .expect("account should generate");
+ let stale_selected_account_id = RadrootsNostrAccountsManager::new(
+ Arc::new(RadrootsNostrMemoryAccountStore::new()),
+ Arc::new(RadrootsNostrSecretVaultMemory::new()),
+ )
+ .expect("secondary memory manager should build")
+ .generate_identity(Some("South field".to_owned()), true)
+ .expect("secondary account should generate");
+ let record = manager
+ .selected_account()
+ .expect("selected account should load")
+ .expect("selected account should exist");
+ let state = radroots_nostr_accounts::prelude::RadrootsNostrAccountStoreState {
+ version: radroots_nostr_accounts::prelude::RADROOTS_NOSTR_ACCOUNTS_STORE_VERSION,
+ selected_account_id: Some(stale_selected_account_id),
+ accounts: vec![record],
+ };
+
+ let projection =
+ blocked_identity_projection_from_store_state(state, &sqlite_store).expect("projection");
+
+ assert_eq!(
+ projection.readiness,
+ IdentityReadiness::Blocked(IdentityBlockedReason::HostVaultUnavailable)
+ );
+ assert!(projection.selected_account.is_none());
+ assert_eq!(projection.roster.len(), 1);
+ assert_eq!(projection.roster[0].account_id, account_id.as_str());
+ }
+
+ fn cleanup_paths(paths: &AppSharedAccountsPaths) {
+ let Some(base) = paths.data_root.ancestors().nth(3).map(PathBuf::from) else {
+ return;
+ };
+ let _ = fs::remove_dir_all(base);
+ }
+}
diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs
@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
+mod accounts;
mod app;
mod menus;
mod runtime;
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -13,9 +13,12 @@ use radroots_app_state::{
AppShellProjection, AppStateCommand, AppStateStore, AppStateStoreError,
InMemoryAppStateRepository,
};
+use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
use thiserror::Error;
use tracing::error;
+use crate::accounts::{DesktopAccountsBootstrapError, bootstrap_desktop_accounts};
+
const APP_DATABASE_FILE_NAME: &str = "app.sqlite3";
#[derive(Clone, Debug)]
@@ -150,6 +153,7 @@ pub struct DesktopAppRuntimeSummary {
struct DesktopAppRuntimeState {
state_store: AppStateStore<InMemoryAppStateRepository>,
+ accounts_manager: Option<RadrootsNostrAccountsManager>,
sqlite_store: Option<AppSqliteStore>,
startup_issue: Option<String>,
}
@@ -160,6 +164,10 @@ impl fmt::Debug for DesktopAppRuntimeState {
.debug_struct("DesktopAppRuntimeState")
.field("state_store", &self.state_store)
.field(
+ "accounts_manager",
+ &self.accounts_manager.as_ref().map(|_| "available"),
+ )
+ .field(
"sqlite_store",
&self.sqlite_store.as_ref().map(|_| "available"),
)
@@ -174,12 +182,17 @@ impl DesktopAppRuntimeState {
let database_path = paths.app.data.join(APP_DATABASE_FILE_NAME);
let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
let mut state_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
+ let accounts_bootstrap = bootstrap_desktop_accounts(&paths.shared_accounts, &sqlite_store)?;
let today_projection = sqlite_store.load_today_agenda(None)?;
let _ =
state_store.apply_in_memory(AppStateCommand::replace_today_agenda(today_projection));
+ let _ = state_store.apply_in_memory(AppStateCommand::replace_identity_projection(
+ accounts_bootstrap.identity_projection,
+ ));
Ok(Self {
state_store,
+ accounts_manager: accounts_bootstrap.accounts_manager,
sqlite_store: Some(sqlite_store),
startup_issue: None,
})
@@ -188,6 +201,7 @@ impl DesktopAppRuntimeState {
fn degraded(error: DesktopAppRuntimeBootstrapError) -> Self {
Self {
state_store: AppStateStore::in_memory(AppShellProjection::default()),
+ accounts_manager: None,
sqlite_store: None,
startup_issue: Some(error.to_string()),
}
@@ -206,6 +220,8 @@ enum DesktopAppRuntimeBootstrapError {
#[error(transparent)]
RuntimePaths(#[from] AppRuntimePathsError),
#[error(transparent)]
+ Accounts(#[from] DesktopAccountsBootstrapError),
+ #[error(transparent)]
Sqlite(#[from] AppSqliteError),
#[error(transparent)]
State(#[from] AppStateStoreError),
@@ -279,6 +295,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(InMemoryAppStateRepository::default())
.expect("in-memory state store should load"),
+ accounts_manager: None,
sqlite_store: Some(
AppSqliteStore::open(DatabaseTarget::InMemory)
.expect("in-memory sqlite store should open"),
@@ -320,6 +337,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(InMemoryAppStateRepository::default())
.expect("in-memory state store should load"),
+ accounts_manager: None,
sqlite_store: Some(
AppSqliteStore::open(DatabaseTarget::InMemory)
.expect("in-memory sqlite store should open"),
@@ -403,6 +421,7 @@ mod tests {
let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState {
state_store: AppStateStore::load(InMemoryAppStateRepository::default())
.expect("in-memory state store should load"),
+ accounts_manager: None,
sqlite_store: Some(
AppSqliteStore::open(DatabaseTarget::InMemory)
.expect("in-memory sqlite store should open"),
diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs
@@ -410,31 +410,46 @@ pub struct AppIdentityProjection {
impl AppIdentityProjection {
pub fn missing() -> Self {
- Self::default()
+ Self::with_readiness(IdentityReadiness::MissingAccount, Vec::new(), None)
+ }
+
+ pub fn missing_with_roster(roster: Vec<AccountSummary>) -> Self {
+ Self::with_readiness(IdentityReadiness::MissingAccount, roster, None)
}
pub fn blocked(reason: IdentityBlockedReason) -> Self {
- Self {
- readiness: IdentityReadiness::Blocked(reason),
- ..Self::default()
- }
+ Self::with_readiness(IdentityReadiness::Blocked(reason), Vec::new(), None)
+ }
+
+ pub fn blocked_with_selection(
+ reason: IdentityBlockedReason,
+ roster: Vec<AccountSummary>,
+ selected_account: Option<SelectedAccountProjection>,
+ ) -> Self {
+ Self::with_readiness(IdentityReadiness::Blocked(reason), roster, selected_account)
}
- pub fn ready(
+ pub fn ready(roster: Vec<AccountSummary>, selected_account: SelectedAccountProjection) -> Self {
+ Self::with_readiness(IdentityReadiness::Ready, roster, Some(selected_account))
+ }
+
+ pub fn with_readiness(
+ readiness: IdentityReadiness,
mut roster: Vec<AccountSummary>,
- selected_account: SelectedAccountProjection,
+ selected_account: Option<SelectedAccountProjection>,
) -> Self {
- if !roster
- .iter()
- .any(|account| account.account_id == selected_account.account.account_id)
+ if let Some(selected_account) = selected_account.as_ref()
+ && !roster
+ .iter()
+ .any(|account| account.account_id == selected_account.account.account_id)
{
roster.insert(0, selected_account.account.clone());
}
Self {
- readiness: IdentityReadiness::Ready,
+ readiness,
roster,
- selected_account: Some(selected_account),
+ selected_account,
}
}
@@ -823,6 +838,49 @@ mod tests {
}
#[test]
+ fn blocked_identity_keeps_selected_account_visible_in_roster() {
+ let selected_account = SelectedAccountProjection::new(
+ AccountSummary {
+ account_id: "acct_blocked".to_owned(),
+ npub: "npub1blocked".to_owned(),
+ label: Some("Blocked account".to_owned()),
+ custody: AccountCustody::LocalManaged,
+ },
+ SelectedSurfaceProjection::new(ActiveSurface::Personal),
+ FarmerActivationProjection::inactive(),
+ );
+ let projection = AppIdentityProjection::blocked_with_selection(
+ IdentityBlockedReason::HostVaultUnavailable,
+ Vec::new(),
+ Some(selected_account.clone()),
+ );
+
+ assert_eq!(
+ projection.readiness,
+ IdentityReadiness::Blocked(IdentityBlockedReason::HostVaultUnavailable)
+ );
+ assert_eq!(projection.roster, vec![selected_account.account.clone()]);
+ assert_eq!(projection.selected_account, Some(selected_account));
+ assert_eq!(projection.startup_gate(), AppStartupGate::Blocked);
+ }
+
+ #[test]
+ fn missing_identity_can_keep_roster_visible_without_selected_account() {
+ let roster = vec![AccountSummary {
+ account_id: "acct_waiting".to_owned(),
+ npub: "npub1waiting".to_owned(),
+ label: Some("Waiting".to_owned()),
+ custody: AccountCustody::LocalManaged,
+ }];
+ let projection = AppIdentityProjection::missing_with_roster(roster.clone());
+
+ assert_eq!(projection.readiness, IdentityReadiness::MissingAccount);
+ assert_eq!(projection.roster, roster);
+ assert!(projection.selected_account.is_none());
+ assert_eq!(projection.startup_gate(), AppStartupGate::SetupRequired);
+ }
+
+ #[test]
fn typed_ids_round_trip_through_strings() {
let uuid = Uuid::parse_str("018f4d61-19b0-7cc4-9d4e-6d0df7c0aa11")
.expect("test uuid should parse");