commit e309980bd60b4dda8b26bba5d82967044494747c
parent 29e05021f8d84ba1d1d885a8528dc90c3a29d223
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 20:25:52 +0000
rhi: migrate trade attestation dependencies
- pin final Radroots package contracts without sibling paths
- move lifecycle paths logging and key custody into the RHI host
- replace the retired Nostr prelude with canonical package APIs
- verify tests clippy docs and the standalone package artifact
Diffstat:
21 files changed, 2085 insertions(+), 1257 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -24,18 +24,6 @@ dependencies = [
]
[[package]]
-name = "ahash"
-version = "0.8.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
-[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -45,21 +33,6 @@ 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"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
-dependencies = [
- "libc",
-]
-
-[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -116,29 +89,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
-name = "arraydeque"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
-
-[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
-name = "async-trait"
-version = "0.1.89"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "async-utility"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -176,22 +132,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
[[package]]
-name = "atomic-waker"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-
-[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
-name = "base64"
-version = "0.21.7"
+name = "base16ct"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
@@ -244,9 +194,6 @@ name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
-dependencies = [
- "serde_core",
-]
[[package]]
name = "block-buffer"
@@ -304,12 +251,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
-name = "cfg_aliases"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
-
-[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -334,19 +275,6 @@ dependencies = [
]
[[package]]
-name = "chrono"
-version = "0.4.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
-dependencies = [
- "iana-time-zone",
- "js-sys",
- "num-traits",
- "wasm-bindgen",
- "windows-link",
-]
-
-[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -404,58 +332,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
-name = "config"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
-dependencies = [
- "async-trait",
- "convert_case",
- "json5",
- "nom",
- "pathdiff",
- "ron",
- "rust-ini",
- "serde",
- "serde_json",
- "toml",
- "yaml-rust2",
-]
-
-[[package]]
-name = "const-random"
-version = "0.1.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
-dependencies = [
- "const-random-macro",
-]
-
-[[package]]
-name = "const-random-macro"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
-dependencies = [
- "getrandom 0.2.17",
- "once_cell",
- "tiny-keccak",
-]
-
-[[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-sys"
-version = "0.8.7"
+name = "const-oid"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cpufeatures"
@@ -482,10 +362,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
-name = "crunchy"
-version = "0.2.4"
+name = "crypto-bigint"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
[[package]]
name = "crypto-common"
@@ -505,6 +391,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -536,27 +432,26 @@ 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 = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
-name = "encoding_rs"
-version = "0.8.35"
+name = "elliptic-curve"
+version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
- "cfg-if",
+ "base16ct",
+ "crypto-bigint",
+ "ff",
+ "generic-array",
+ "group",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
]
[[package]]
@@ -582,6 +477,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -633,6 +538,17 @@ 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"
@@ -674,6 +590,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
+ "zeroize",
]
[[package]]
@@ -696,11 +613,9 @@ 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]]
@@ -729,13 +644,14 @@ dependencies = [
]
[[package]]
-name = "hashbrown"
-version = "0.14.5"
+name = "group"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
- "ahash",
- "allocator-api2",
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
]
[[package]]
@@ -754,15 +670,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[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 = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -803,120 +710,12 @@ dependencies = [
]
[[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.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
-dependencies = [
- "atomic-waker",
- "bytes",
- "futures-channel",
- "futures-core",
- "http",
- "http-body",
- "httparse",
- "itoa",
- "pin-project-lite",
- "pin-utils",
- "smallvec",
- "tokio",
- "want",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.27.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
-dependencies = [
- "http",
- "hyper",
- "hyper-util",
- "rustls",
- "rustls-pki-types",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "webpki-roots 1.0.6",
-]
-
-[[package]]
-name = "hyper-util"
-version = "0.1.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-channel",
- "futures-util",
- "http",
- "http-body",
- "hyper",
- "ipnet",
- "libc",
- "percent-encoding",
- "pin-project-lite",
- "socket2",
- "tokio",
- "tower-service",
- "tracing",
-]
-
-[[package]]
-name = "iana-time-zone"
-version = "0.1.65"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
-dependencies = [
- "android_system_properties",
- "core-foundation-sys",
- "iana-time-zone-haiku",
- "js-sys",
- "log",
- "wasm-bindgen",
- "windows-core",
-]
-
-[[package]]
-name = "iana-time-zone-haiku"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
-dependencies = [
- "cc",
-]
-
-[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1005,6 +804,16 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
@@ -1059,22 +868,6 @@ dependencies = [
]
[[package]]
-name = "ipnet"
-version = "2.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
-
-[[package]]
-name = "iri-string"
-version = "0.7.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
-dependencies = [
- "memchr",
- "serde",
-]
-
-[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1103,14 +896,13 @@ dependencies = [
]
[[package]]
-name = "json5"
-version = "0.4.1"
+name = "k256"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
dependencies = [
- "pest",
- "pest_derive",
- "serde",
+ "cfg-if",
+ "elliptic-curve",
]
[[package]]
@@ -1165,12 +957,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
[[package]]
-name = "lru-slab"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
-
-[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1192,12 +978,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
-name = "minimal-lexical"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
-
-[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1215,23 +995,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
[[package]]
-name = "nom"
-version = "7.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
-dependencies = [
- "memchr",
- "minimal-lexical",
-]
-
-[[package]]
name = "nostr"
version = "0.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
dependencies = [
"aes",
- "base64 0.22.1",
+ "base64",
"bech32",
"bip39",
"bitcoin_hashes",
@@ -1247,6 +1017,7 @@ dependencies = [
"serde_json",
"unicode-normalization",
"url",
+ "url-fork",
]
[[package]]
@@ -1271,9 +1042,9 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
-version = "0.44.0"
+version = "0.44.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
+checksum = "c85c54d6ca9aae4ae2bf19a7663ba9db5f45f783f1d24aff55f006386b8b99a1"
dependencies = [
"async-utility",
"async-wsocket",
@@ -1345,16 +1116,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[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 = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1389,12 +1150,6 @@ dependencies = [
]
[[package]]
-name = "pathdiff"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
-
-[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1411,61 +1166,12 @@ 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",
-]
-
-[[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 = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-
-[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1520,61 +1226,6 @@ dependencies = [
]
[[package]]
-name = "quinn"
-version = "0.11.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
-dependencies = [
- "bytes",
- "cfg_aliases",
- "pin-project-lite",
- "quinn-proto",
- "quinn-udp",
- "rustc-hash",
- "rustls",
- "socket2",
- "thiserror 2.0.18",
- "tokio",
- "tracing",
- "web-time",
-]
-
-[[package]]
-name = "quinn-proto"
-version = "0.11.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
-dependencies = [
- "bytes",
- "getrandom 0.3.4",
- "lru-slab",
- "rand 0.9.2",
- "ring",
- "rustc-hash",
- "rustls",
- "rustls-pki-types",
- "slab",
- "thiserror 2.0.18",
- "tinyvec",
- "tracing",
- "web-time",
-]
-
-[[package]]
-name = "quinn-udp"
-version = "0.5.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
-dependencies = [
- "cfg_aliases",
- "libc",
- "once_cell",
- "socket2",
- "tracing",
- "windows-sys 0.60.2",
-]
-
-[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1596,15 +1247,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
-name = "radroots_authority"
-version = "1.0.0-alpha.1"
-dependencies = [
- "radroots_event",
-]
-
-[[package]]
name = "radroots_blossom"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
"mediatype",
"serde",
@@ -1615,21 +1259,22 @@ dependencies = [
[[package]]
name = "radroots_core"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
"rust_decimal",
- "rust_decimal_macros",
"serde",
]
[[package]]
name = "radroots_event"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
"hex",
"jiff-tzdb",
"radroots_blossom",
"radroots_core",
+ "radroots_identity",
+ "radroots_protocol",
"serde",
"serde_json",
"sha2",
@@ -1639,119 +1284,69 @@ dependencies = [
[[package]]
name = "radroots_event_codec"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
- "nostr",
"radroots_blossom",
"radroots_core",
"radroots_event",
+ "radroots_identity",
+ "radroots_protocol",
+ "secp256k1",
"serde",
"serde_json",
]
[[package]]
name = "radroots_identity"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
- "nostr",
- "radroots_protected_store",
- "radroots_runtime",
- "radroots_runtime_paths",
- "radroots_secret_vault",
+ "k256",
"serde",
- "serde_json",
- "thiserror 1.0.69",
- "tracing",
-]
-
-[[package]]
-name = "radroots_log"
-version = "1.0.0-alpha.1"
-dependencies = [
- "chrono",
- "serde_json",
- "thiserror 1.0.69",
- "tracing",
- "tracing-appender",
- "tracing-subscriber",
+ "thiserror 2.0.18",
]
[[package]]
name = "radroots_nostr"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
"nostr",
- "nostr-sdk",
"radroots_event",
"radroots_event_codec",
"radroots_identity",
- "reqwest",
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
-name = "radroots_protected_store"
-version = "1.0.0-alpha.1"
+name = "radroots_protocol"
+version = "0.1.0-alpha"
dependencies = [
- "chacha20poly1305",
- "getrandom 0.2.17",
- "radroots_secret_vault",
"serde",
- "serde_json",
- "zeroize",
]
[[package]]
-name = "radroots_runtime"
-version = "1.0.0-alpha.1"
+name = "radroots_secrets"
+version = "0.1.0-alpha"
dependencies = [
- "anyhow",
"chacha20poly1305",
- "clap",
- "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",
- "tracing",
"zeroize",
]
[[package]]
-name = "radroots_runtime_paths"
-version = "1.0.0-alpha.1"
-dependencies = [
- "serde",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "radroots_secret_vault"
-version = "1.0.0-alpha.1"
-
-[[package]]
name = "radroots_trade"
-version = "1.0.0-alpha.1"
+version = "0.1.0-alpha"
dependencies = [
- "base64 0.22.1",
+ "base64",
"hex",
- "radroots_authority",
"radroots_core",
"radroots_event",
"radroots_event_codec",
+ "radroots_identity",
"serde",
"serde_json",
"sha2",
- "thiserror 1.0.69",
]
[[package]]
@@ -1827,71 +1422,36 @@ name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
-]
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
-
-[[package]]
-name = "reqwest"
-version = "0.12.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
-dependencies = [
- "base64 0.22.1",
- "bytes",
- "futures-core",
- "http",
- "http-body",
- "http-body-util",
- "hyper",
- "hyper-rustls",
- "hyper-util",
- "js-sys",
- "log",
- "percent-encoding",
- "pin-project-lite",
- "quinn",
- "rustls",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "sync_wrapper",
- "tokio",
- "tokio-rustls",
- "tower",
- "tower-http",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "webpki-roots 1.0.6",
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
]
[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
name = "rhi"
version = "0.1.0"
dependencies = [
"anyhow",
+ "chacha20poly1305",
"clap",
- "radroots_core",
+ "futures-executor",
+ "nostr",
+ "nostr-sdk",
"radroots_event",
"radroots_event_codec",
"radroots_identity",
- "radroots_log",
"radroots_nostr",
- "radroots_runtime",
- "radroots_runtime_paths",
+ "radroots_protocol",
+ "radroots_secrets",
"radroots_trade",
+ "rand 0.9.2",
"serde",
"serde_json",
"sha2",
@@ -1900,6 +1460,9 @@ dependencies = [
"tokio",
"toml",
"tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "zeroize",
]
[[package]]
@@ -1917,28 +1480,6 @@ 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",
- "serde",
- "serde_derive",
-]
-
-[[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.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1950,22 +1491,6 @@ dependencies = [
]
[[package]]
-name = "rust_decimal_macros"
-version = "1.40.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519"
-dependencies = [
- "quote",
- "syn",
-]
-
-[[package]]
-name = "rustc-hash"
-version = "2.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
-
-[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1998,7 +1523,6 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
- "web-time",
"zeroize",
]
@@ -2020,12 +1544,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
-name = "ryu"
-version = "1.0.23"
-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"
@@ -2053,6 +1571,19 @@ dependencies = [
]
[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "secp256k1"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2131,18 +1662,6 @@ dependencies = [
]
[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2241,15 +1760,6 @@ dependencies = [
]
[[package]]
-name = "sync_wrapper"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
-dependencies = [
- "futures-core",
-]
-
-[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2354,15 +1864,6 @@ dependencies = [
]
[[package]]
-name = "tiny-keccak"
-version = "2.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
-dependencies = [
- "crunchy",
-]
-
-[[package]]
name = "tinystr"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2495,51 +1996,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
-name = "tower"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
-dependencies = [
- "futures-core",
- "futures-util",
- "pin-project-lite",
- "sync_wrapper",
- "tokio",
- "tower-layer",
- "tower-service",
-]
-
-[[package]]
-name = "tower-http"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
-dependencies = [
- "bitflags",
- "bytes",
- "futures-util",
- "http",
- "http-body",
- "iri-string",
- "pin-project-lite",
- "tower",
- "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"
@@ -2613,12 +2069,6 @@ dependencies = [
]
[[package]]
-name = "try-lock"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-
-[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2644,10 +2094,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
-name = "ucd-trie"
-version = "0.1.7"
+name = "unicode-bidi"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-general-category"
@@ -2671,12 +2121,6 @@ dependencies = [
]
[[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2705,13 +2149,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
- "idna",
+ "idna 1.1.0",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]
+name = "url-fork"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fa3323c39b8e786154d3000b70ae9af0e9bd746c9791456da0d4a1f68ad89d6"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.5.0",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2742,15 +2198,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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"
@@ -2878,16 +2325,6 @@ dependencies = [
]
[[package]]
-name = "web-time"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
-dependencies = [
- "js-sys",
- "wasm-bindgen",
-]
-
-[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2906,80 +2343,18 @@ dependencies = [
]
[[package]]
-name = "windows-core"
-version = "0.62.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
-[[package]]
-name = "windows-implement"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "windows-interface"
-version = "0.59.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "windows-result"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
-dependencies = [
- "windows-link",
-]
-
-[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-sys"
-version = "0.60.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
-dependencies = [
- "windows-targets 0.53.5",
+ "windows-targets",
]
[[package]]
@@ -2997,31 +2372,14 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm 0.52.6",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.53.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
-dependencies = [
- "windows-link",
- "windows_aarch64_gnullvm 0.53.1",
- "windows_aarch64_msvc 0.53.1",
- "windows_i686_gnu 0.53.1",
- "windows_i686_gnullvm 0.53.1",
- "windows_i686_msvc 0.53.1",
- "windows_x86_64_gnu 0.53.1",
- "windows_x86_64_gnullvm 0.53.1",
- "windows_x86_64_msvc 0.53.1",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
]
[[package]]
@@ -3031,96 +2389,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-
-[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
-name = "windows_aarch64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-
-[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
-name = "windows_i686_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
-
-[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
-name = "windows_i686_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-
-[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
-name = "windows_i686_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-
-[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
-name = "windows_x86_64_gnu"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-
-[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-
-[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.53.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
-
-[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3224,17 +2534,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[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",
-]
-
-[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3342,3 +2641,123 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[patch.unused]]
+name = "radroots_authority"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_event_store"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_geonames"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_log"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_mesh"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_nostr_accounts"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_nostr_connect"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_nostr_runtime"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_nostr_signer"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_nostrdb"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_outbox"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_protected_store"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_protocol_contract_v1"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_replica_schema"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_replica_store"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_replica_sync"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_runtime"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_runtime_paths"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_runtime_store"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_secret_vault"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_signing"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_sql_core"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_storage"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_storage_sqlite"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_sync"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_test_fixtures"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_transport"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_transport_nostr"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_transport_publish_protocol"
+version = "0.1.0-alpha"
+
+[[patch.unused]]
+name = "radroots_transport_reticulum"
+version = "0.1.0-alpha"
diff --git a/Cargo.toml b/Cargo.toml
@@ -3,23 +3,21 @@ name = "rhi"
version = "0.1.0"
edition = "2024"
authors = ["Radroots Authors"]
-rust-version = "1.97.0"
+rust-version = "1.97.1"
license = "AGPL-3.0-or-later"
description = "Radroots trade agreement attestation worker"
[workspace]
-resolver = "2"
+resolver = "3"
[workspace.dependencies]
-radroots_core = { version = "=1.0.0-alpha.1", path = "../lib/crates/core" }
-radroots_event = { version = "=1.0.0-alpha.1", path = "../lib/crates/event" }
-radroots_event_codec = { version = "=1.0.0-alpha.1", path = "../lib/crates/event_codec" }
-radroots_identity = { version = "=1.0.0-alpha.1", path = "../lib/crates/identity" }
-radroots_log = { version = "=1.0.0-alpha.1", path = "../lib/crates/log" }
-radroots_nostr = { version = "=1.0.0-alpha.1", path = "../lib/crates/nostr" }
-radroots_runtime = { version = "=1.0.0-alpha.1", path = "../lib/crates/runtime" }
-radroots_runtime_paths = { version = "=1.0.0-alpha.1", path = "../lib/crates/runtime_paths" }
-radroots_trade = { version = "=1.0.0-alpha.1", path = "../lib/crates/trade" }
+radroots_event = "=0.1.0-alpha"
+radroots_event_codec = "=0.1.0-alpha"
+radroots_identity = "=0.1.0-alpha"
+radroots_nostr = "=0.1.0-alpha"
+radroots_protocol = "=0.1.0-alpha"
+radroots_secrets = "=0.1.0-alpha"
+radroots_trade = "=0.1.0-alpha"
[features]
default = []
@@ -28,25 +26,31 @@ default = []
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[dependencies]
-radroots_core = { workspace = true, features = ["std", "serde"] }
radroots_event = { workspace = true, features = ["serde"] }
-radroots_event_codec = { workspace = true, features = ["nostr", "serde_json"] }
+radroots_event_codec = { workspace = true, features = ["json"] }
radroots_identity = { workspace = true }
-radroots_log = { workspace = true }
-radroots_nostr = { workspace = true, features = ["client", "codec", "events", "http"] }
-radroots_runtime = { workspace = true, features = ["cli"] }
-radroots_runtime_paths = { workspace = true }
+radroots_nostr = { workspace = true, features = ["events"] }
+radroots_protocol = { workspace = true }
+radroots_secrets = { workspace = true }
radroots_trade = { workspace = true }
anyhow = { version = "1" }
+chacha20poly1305 = { version = "0.10" }
clap = { version = "4", features = ["derive"] }
+futures-executor = { version = "0.3" }
+nostr = { version = "0.44.2", features = ["nip49"] }
+nostr-sdk = { version = "0.44.1" }
+rand = { version = "0.9" }
serde = { version = "1", default-features = false }
serde_json = { version = "1", default-features = false }
sha2 = { version = "0.10" }
tokio = { version = "1", features = ["full"] }
thiserror = { version = "2" }
+tempfile = { version = "3" }
toml = { version = "0.8" }
tracing = { version = "0.1" }
+tracing-appender = { version = "0.2" }
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+zeroize = { version = "1" }
[dev-dependencies]
-tempfile = { version = "3" }
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
@@ -1,2 +1,2 @@
[toolchain]
-channel = "1.97.0"
+channel = "1.97.1"
diff --git a/src/adapters/nostr/event.rs b/src/adapters/nostr/event.rs
@@ -1,16 +1,16 @@
+use crate::host_nostr::{Event, Kind};
use radroots_event_codec::job::traits::{JobEventBorrow, JobEventLike};
-use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKind};
#[derive(Clone, Debug)]
pub struct NostrEventAdapter<'a> {
- evt: &'a RadrootsNostrEvent,
+ evt: &'a Event,
id_hex: String,
author_hex: String,
}
impl<'a> NostrEventAdapter<'a> {
#[inline]
- pub fn new(evt: &'a RadrootsNostrEvent) -> Self {
+ pub fn new(evt: &'a Event) -> Self {
Self {
evt,
id_hex: evt.id.to_hex(),
@@ -30,12 +30,12 @@ impl<'a> NostrEventAdapter<'a> {
impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> {
#[inline]
- fn raw_id(&'a self) -> &'a str {
- &self.id_hex
+ fn raw_id(&'a self) -> String {
+ self.id_hex.clone()
}
#[inline]
- fn raw_author(&'a self) -> &'a str {
- &self.author_hex
+ fn raw_author(&'a self) -> String {
+ self.author_hex.clone()
}
#[inline]
fn raw_content(&'a self) -> &'a str {
@@ -44,7 +44,7 @@ impl<'a> JobEventBorrow<'a> for NostrEventAdapter<'a> {
#[inline]
fn raw_kind(&'a self) -> u32 {
match self.evt.kind {
- RadrootsNostrKind::Custom(v) => v as u32,
+ Kind::Custom(v) => v as u32,
_ => 0,
}
}
@@ -62,7 +62,7 @@ impl JobEventLike for NostrEventAdapter<'_> {
}
fn raw_kind(&self) -> u32 {
match self.evt.kind {
- RadrootsNostrKind::Custom(v) => v as u32,
+ Kind::Custom(v) => v as u32,
_ => 0,
}
}
@@ -81,18 +81,11 @@ impl JobEventLike for NostrEventAdapter<'_> {
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::NostrEventAdapter;
+ use crate::host_nostr::{Event, GenericBuilder, Keys, Kind, Tag, TagKind};
use radroots_event_codec::job::traits::{JobEventBorrow, JobEventLike};
- use radroots_nostr::prelude::{
- RadrootsNostrEvent, RadrootsNostrGenericEventBuilder, RadrootsNostrKeys, RadrootsNostrKind,
- RadrootsNostrTag, RadrootsNostrTagKind,
- };
- fn build_event(
- keys: &RadrootsNostrKeys,
- kind: RadrootsNostrKind,
- tags: Vec<RadrootsNostrTag>,
- ) -> RadrootsNostrEvent {
- RadrootsNostrGenericEventBuilder::new(kind, "content")
+ fn build_event(keys: &Keys, kind: Kind, tags: Vec<Tag>) -> Event {
+ GenericBuilder::new(kind, "content")
.tags(tags)
.sign_with_keys(keys)
.expect("event must sign")
@@ -100,14 +93,11 @@ mod tests {
#[test]
fn adapter_exposes_borrow_and_owned_fields_for_custom_kind() {
- let keys = RadrootsNostrKeys::generate();
- let recipient = RadrootsNostrKeys::generate();
+ let keys = Keys::generate();
+ let recipient = Keys::generate();
let recipient_hex = recipient.public_key().to_hex();
- let tags = vec![RadrootsNostrTag::custom(
- RadrootsNostrTagKind::p(),
- vec![recipient_hex.clone()],
- )];
- let event = build_event(&keys, RadrootsNostrKind::Custom(5322), tags);
+ let tags = vec![Tag::custom(TagKind::p(), vec![recipient_hex.clone()])];
+ let event = build_event(&keys, Kind::Custom(5322), tags);
let adapter = NostrEventAdapter::new(&event);
assert_eq!(JobEventBorrow::raw_id(&adapter), event.id.to_hex());
@@ -131,8 +121,8 @@ mod tests {
#[test]
fn adapter_maps_non_custom_kind_to_zero() {
- let keys = RadrootsNostrKeys::generate();
- let event = build_event(&keys, RadrootsNostrKind::Repost, Vec::new());
+ let keys = Keys::generate();
+ let event = build_event(&keys, Kind::Repost, Vec::new());
let adapter = NostrEventAdapter::new(&event);
assert_eq!(JobEventBorrow::raw_kind(&adapter), 0);
diff --git a/src/cli.rs b/src/cli.rs
@@ -1,5 +1,5 @@
+use crate::host_runtime::ServiceCliArgs;
use clap::Parser;
-use radroots_runtime::RadrootsServiceCliArgs;
use std::path::PathBuf;
#[derive(Parser, Debug, Clone)]
@@ -12,7 +12,7 @@ pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
#[command(flatten)]
- pub service: RadrootsServiceCliArgs,
+ pub service: ServiceCliArgs,
}
#[derive(clap::Subcommand, Debug, Clone)]
diff --git a/src/config.rs b/src/config.rs
@@ -1,6 +1,6 @@
+use crate::host_nostr::Metadata;
+use crate::host_runtime::{BackoffConfig, NostrServiceConfig};
use anyhow::{Context, Result, bail};
-use radroots_nostr::prelude::RadrootsNostrMetadata;
-use radroots_runtime::{BackoffConfig, RadrootsNostrServiceConfig};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
@@ -83,8 +83,8 @@ struct RawServiceConfig {
}
impl RawServiceConfig {
- fn into_service_config(self) -> RadrootsNostrServiceConfig {
- RadrootsNostrServiceConfig {
+ fn into_service_config(self) -> NostrServiceConfig {
+ NostrServiceConfig {
logs_dir: self.logging.output_dir.display().to_string(),
relays: self.relays.urls,
nip89_identifier: self.nostr.nip89.identifier,
@@ -96,7 +96,7 @@ impl RawServiceConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Configuration {
#[serde(flatten)]
- pub service: RadrootsNostrServiceConfig,
+ pub service: NostrServiceConfig,
pub logging: LoggingConfig,
#[serde(default)]
pub subscriber: SubscriberConfig,
@@ -184,7 +184,7 @@ impl Default for SubscriberStateConfig {
#[derive(Debug, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
struct RawSettings {
- pub metadata: RadrootsNostrMetadata,
+ pub metadata: Metadata,
#[serde(default)]
pub logging: RawLoggingConfig,
#[serde(default)]
@@ -221,14 +221,14 @@ impl RawSettings {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
- pub metadata: RadrootsNostrMetadata,
+ pub metadata: Metadata,
pub config: Configuration,
}
fn load_settings_from_path_with_resolver(
path: &Path,
- resolver: &radroots_runtime_paths::RadrootsPathResolver,
- profile: radroots_runtime_paths::RadrootsPathProfile,
+ resolver: &crate::host_paths::RadrootsPathResolver,
+ profile: crate::host_paths::RadrootsPathProfile,
repo_local_root: Option<&Path>,
) -> Result<Settings> {
let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?;
@@ -245,7 +245,7 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
let (profile, repo_local_root) = crate::paths::process_path_selection()?;
load_settings_from_path_with_resolver(
path,
- &radroots_runtime_paths::RadrootsPathResolver::current(),
+ &crate::host_paths::RadrootsPathResolver::current(),
profile,
repo_local_root.as_deref(),
)
@@ -255,14 +255,14 @@ pub fn load_settings_from_path(path: &Path) -> Result<Settings> {
mod tests {
use super::load_settings_from_path_with_resolver;
use crate::features::trade_agreement_attestation::TradeAgreementAttestationBackend;
+ use crate::host_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform, RadrootsRuntimeNamespace,
+ };
use crate::paths::{
default_subscriber_state_path_for_process, resolve_runtime_paths_with_resolver,
runtime_contract_with_resolver,
};
- use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPlatform, RadrootsRuntimeNamespace,
- };
use std::path::PathBuf;
fn linux_resolver() -> RadrootsPathResolver {
diff --git a/src/features/trade_agreement_attestation.rs b/src/features/trade_agreement_attestation.rs
@@ -7,25 +7,25 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
-use anyhow::{Result, anyhow};
-use radroots_event::ids::{
- RadrootsAddressableCoordinate, RadrootsEventId, RadrootsPublicKey, RadrootsTradeId,
- RadrootsTradeMutationId,
+use crate::host_nostr::{
+ Client, Event, Filter, Keys, Kind, RelayPoolNotification, SubscriptionId, Timestamp,
};
-use radroots_event::kinds::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind};
+use anyhow::{Result, anyhow};
+use radroots_event::envelope::kind::{TRADE_MUTATION_EVENT_KINDS, is_trade_mutation_event_kind};
+use radroots_event::id::{AddressableCoordinate, EventId, MutationId, TradeId};
use radroots_event::trade::{
- RadrootsTradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content,
+ TradeMutationEnvelopeV1, canonical_jcs_value, trade_mutation_from_canonical_content,
+};
+use radroots_identity::PublicKey;
+use radroots_trade::evidence::{
+ RadrootsTradeAttestationResultV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
};
-use radroots_nostr::prelude::{
- RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKeys,
- RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrSubscriptionId,
- RadrootsNostrTimestamp,
+use radroots_trade::model::{
+ RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, RadrootsTradeProjectionV1,
};
-use radroots_trade::workflow::{
+use radroots_trade::reducer::{
RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
- RadrootsTradeAgreementStateV1, RadrootsTradeAttestationResultV1,
- RadrootsTradeAttestationStateV1, RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1,
- RadrootsTradeProjectionV1, RadrootsTradeReductionInputV1, reduce_trade_records,
+ RadrootsTradeReductionInputV1, reduce_trade_records,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -76,10 +76,10 @@ impl TradeAgreementAttestationPolicy {
self.validator_set_event_id.as_deref(),
) {
(Some(addr), Some(event_id)) => {
- RadrootsAddressableCoordinate::parse(addr).map_err(|_| {
+ AddressableCoordinate::parse(addr).map_err(|_| {
TradeAgreementAttestationError::InvalidValidatorSetBinding("validator_set_addr")
})?;
- RadrootsEventId::parse(event_id).map_err(|_| {
+ EventId::parse(event_id).map_err(|_| {
TradeAgreementAttestationError::InvalidValidatorSetBinding(
"validator_set_event_id",
)
@@ -223,7 +223,7 @@ pub enum TradeAgreementAttestationError {
#[error("invalid signed event")]
InvalidSignedEvent,
#[error("trade protocol error: {0}")]
- TradeProtocol(#[from] radroots_event::trade::RadrootsTradeProtocolError),
+ TradeProtocol(#[from] radroots_event::trade::TradeProtocolError),
#[error("serde error: {0}")]
Serde(#[from] serde_json::Error),
#[error("state error: {0}")]
@@ -305,18 +305,16 @@ impl TradeAgreementAttestationRuntime {
self.persist().await
}
- pub async fn recovery_filter(&self, kinds: Vec<RadrootsNostrKind>) -> RadrootsNostrFilter {
+ pub async fn recovery_filter(&self, kinds: Vec<Kind>) -> Filter {
let since = {
let state = self.state.lock().await;
state.replay_since(
- RadrootsNostrTimestamp::now().as_secs(),
+ Timestamp::now().as_secs(),
self.config.replay_window_secs,
self.config.replay_overlap_secs,
)
};
- RadrootsNostrFilter::new()
- .kinds(kinds)
- .since(RadrootsNostrTimestamp::from(since))
+ Filter::new().kinds(kinds).since(Timestamp::from(since))
}
pub async fn reports(&self) -> Vec<TradeAgreementAttestationReportV1> {
@@ -331,17 +329,17 @@ impl TradeAgreementAttestationRuntime {
async fn observe_mutation_event(
&self,
- event: &RadrootsNostrEvent,
- mutation: &RadrootsTradeMutationEnvelopeV1,
- mutation_id: &RadrootsTradeMutationId,
+ event: &Event,
+ mutation: &TradeMutationEnvelopeV1,
+ mutation_id: &MutationId,
kind: u32,
) -> Result<bool, TradeAgreementAttestationRuntimeError> {
let observation = TradeMutationObservationV1 {
event_id: event.id.to_hex(),
event_kind: kind,
event_pubkey: event.pubkey.to_hex(),
- trade_id: mutation.trade_id.as_str().to_owned(),
- mutation_id: mutation_id.as_str().to_owned(),
+ trade_id: mutation.trade_id.to_hex(),
+ mutation_id: mutation_id.to_hex(),
content: event.content.clone(),
observed_at_unix_s: event.created_at.as_secs(),
};
@@ -357,32 +355,33 @@ impl TradeAgreementAttestationRuntime {
async fn reduce_trade(
&self,
- trade_id: &RadrootsTradeId,
+ trade_id: &TradeId,
) -> Result<RadrootsTradeProjectionV1, TradeAgreementAttestationError> {
let observations = {
let state = self.state.lock().await;
state
.mutation_events
.values()
- .filter(|observation| observation.trade_id == trade_id.as_str())
+ .filter(|observation| observation.trade_id == trade_id.to_hex().as_str())
.cloned()
.collect::<Vec<_>>()
};
- let mut input = RadrootsTradeReductionInputV1::new(trade_id.clone());
- input.evidence_state = RadrootsTradeEvidenceStateV1::Complete;
- input.mutations = observations
+ let mutations = observations
.iter()
.map(|observation| {
let mutation = trade_mutation_from_canonical_content(observation.content.as_str())?;
- let transport_event_id = RadrootsEventId::parse(observation.event_id.as_str())
+ let transport_event_id = EventId::parse(observation.event_id.as_str())
.map(Some)
.map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?;
- Ok(RadrootsTradeMutationRecordV1 {
+ Ok(RadrootsTradeMutationRecordV1::new(
transport_event_id,
mutation,
- })
+ ))
})
.collect::<Result<Vec<_>, TradeAgreementAttestationError>>()?;
+ let input = RadrootsTradeReductionInputV1::new(*trade_id)
+ .with_evidence_state(RadrootsTradeEvidenceStateV1::Complete)
+ .with_mutations(mutations);
Ok(reduce_trade_records(input))
}
@@ -483,7 +482,7 @@ fn temp_state_path(path: &Path) -> Result<PathBuf, TradeAgreementAttestationRunt
}
pub async fn handle_trade_mutation_event(
- event: RadrootsNostrEvent,
+ event: Event,
runtime: TradeAgreementAttestationRuntime,
policy: &TradeAgreementAttestationPolicy,
) -> Result<Option<TradeAgreementAttestationReportV1>, TradeAgreementAttestationError> {
@@ -496,14 +495,13 @@ pub async fn handle_trade_mutation_event(
if mutation.mutation_kind().nostr_kind() != kind {
return Err(TradeAgreementAttestationError::UnsupportedKind);
}
- let event_author = RadrootsPublicKey::parse(event.pubkey.to_hex())
+ let event_author = PublicKey::from_hex(&event.pubkey.to_hex())
.map_err(|_| TradeAgreementAttestationError::InvalidSignedEvent)?;
if event_author != mutation.author_pubkey {
return Err(TradeAgreementAttestationError::InvalidEventAuthor);
}
let mutation_id = mutation
.mutation_id
- .clone()
.ok_or(TradeAgreementAttestationError::MissingMutationId)?;
if !runtime
.observe_mutation_event(&event, &mutation, &mutation_id, kind)
@@ -513,10 +511,10 @@ pub async fn handle_trade_mutation_event(
}
let projection = runtime.reduce_trade(&mutation.trade_id).await?;
let Some(claim_id) = projection
- .active_agreement_claim_ids
+ .active_agreement_claim_ids()
.iter()
.find(|claim_id| **claim_id == mutation_id)
- .or_else(|| projection.active_agreement_claim_ids.first())
+ .or_else(|| projection.active_agreement_claim_ids().first())
.cloned()
else {
return Ok(None);
@@ -528,14 +526,14 @@ pub async fn handle_trade_mutation_event(
pub fn attest_projection_claim(
projection: &RadrootsTradeProjectionV1,
- claim_mutation_id: &RadrootsTradeMutationId,
+ claim_mutation_id: &MutationId,
policy: &TradeAgreementAttestationPolicy,
) -> Result<TradeAgreementAttestationReportV1, TradeAgreementAttestationError> {
policy.validate()?;
if !projection
- .agreement_claims
+ .agreement_claims()
.iter()
- .any(|claim| claim.claim_mutation_id == *claim_mutation_id)
+ .any(|claim| claim.claim_mutation_id() == claim_mutation_id)
{
return Err(TradeAgreementAttestationError::MissingAgreementClaim);
}
@@ -544,32 +542,34 @@ pub fn attest_projection_claim(
schema_version: RHI_AGREEMENT_ATTESTATION_REPORT_VERSION,
reducer_contract_id: RADROOTS_TRADE_REDUCER_CONTRACT_ID.to_owned(),
reducer_version: RADROOTS_TRADE_REDUCER_VERSION,
- trade_id: projection.trade_id.as_str().to_owned(),
- claim_mutation_id: claim_mutation_id.as_str().to_owned(),
- projection_digest: projection.projection_digest.clone(),
- agreement_state: projection.agreement_state,
- attestation_state_before_report: projection.attestation_state,
- active_agreement_claim_ids: mutation_ids_to_strings(&projection.active_agreement_claim_ids),
- contested_claim_ids: mutation_ids_to_strings(&projection.contested_claim_ids),
- cancelled_claim_ids: mutation_ids_to_strings(&projection.cancelled_claim_ids),
- evidence_state: projection.evidence_state,
+ trade_id: projection.trade_id().to_hex(),
+ claim_mutation_id: claim_mutation_id.to_hex(),
+ projection_digest: projection.projection_digest().to_owned(),
+ agreement_state: projection.agreement_state(),
+ attestation_state_before_report: projection.attestation_state(),
+ active_agreement_claim_ids: mutation_ids_to_strings(
+ projection.active_agreement_claim_ids(),
+ ),
+ contested_claim_ids: mutation_ids_to_strings(projection.contested_claim_ids()),
+ cancelled_claim_ids: mutation_ids_to_strings(projection.cancelled_claim_ids()),
+ evidence_state: projection.evidence_state(),
validator_set: policy.validator_set_binding()?,
};
let statement_hash = hash_canonical_value(
b"radroots:rhi-agreement-attestation-statement:v1\0",
&statement,
)?;
- let result = if projection.agreement_state == RadrootsTradeAgreementStateV1::Agreed
+ let result = if projection.agreement_state() == RadrootsTradeAgreementStateV1::Agreed
&& projection
- .active_agreement_claim_ids
+ .active_agreement_claim_ids()
.iter()
.any(|claim| claim == claim_mutation_id)
&& !projection
- .contested_claim_ids
+ .contested_claim_ids()
.iter()
.any(|claim| claim == claim_mutation_id)
&& !projection
- .cancelled_claim_ids
+ .cancelled_claim_ids()
.iter()
.any(|claim| claim == claim_mutation_id)
{
@@ -609,16 +609,13 @@ pub fn trade_mutation_subscription_kinds() -> Vec<u32> {
TRADE_MUTATION_EVENT_KINDS.to_vec()
}
-fn mutation_ids_to_strings(values: &[RadrootsTradeMutationId]) -> Vec<String> {
- values
- .iter()
- .map(|value| value.as_str().to_owned())
- .collect()
+fn mutation_ids_to_strings(values: &[MutationId]) -> Vec<String> {
+ values.iter().map(MutationId::to_hex).collect()
}
-fn event_kind_u32(event: &RadrootsNostrEvent) -> Result<u32, TradeAgreementAttestationError> {
+fn event_kind_u32(event: &Event) -> Result<u32, TradeAgreementAttestationError> {
match event.kind {
- RadrootsNostrKind::Custom(value) => Ok(u32::from(value)),
+ Kind::Custom(value) => Ok(u32::from(value)),
_ => Err(TradeAgreementAttestationError::UnsupportedKind),
}
}
@@ -651,23 +648,16 @@ fn hash_canonical_value(
}
fn map_notification_recv_result(
- result: Result<RadrootsNostrRelayPoolNotification, tokio::sync::broadcast::error::RecvError>,
-) -> Result<RadrootsNostrRelayPoolNotification, ()> {
+ result: Result<RelayPoolNotification, tokio::sync::broadcast::error::RecvError>,
+) -> Result<RelayPoolNotification, ()> {
result.map_err(|_| ())
}
-async fn subscribe_io(
- client: &RadrootsNostrClient,
- filter: RadrootsNostrFilter,
-) -> Result<RadrootsNostrSubscriptionId> {
- let subscription = client.subscribe(filter, None).await?;
- Ok(subscription.val)
+async fn subscribe_io(client: &Client, filter: Filter) -> Result<SubscriptionId> {
+ client.subscribe(filter).await.map_err(Into::into)
}
-async fn unsubscribe_io(
- client: &RadrootsNostrClient,
- subscription_id: &RadrootsNostrSubscriptionId,
-) {
+async fn unsubscribe_io(client: &Client, subscription_id: &SubscriptionId) {
client.unsubscribe(subscription_id).await;
}
@@ -676,7 +666,7 @@ fn should_delay_before_event_handle() -> bool {
}
async fn process_event_notification(
- event: RadrootsNostrEvent,
+ event: Event,
runtime: TradeAgreementAttestationRuntime,
policy: TradeAgreementAttestationPolicy,
) -> Result<()> {
@@ -698,8 +688,8 @@ async fn process_event_notification(
}
pub async fn subscriber(
- client: RadrootsNostrClient,
- _keys: RadrootsNostrKeys,
+ client: Client,
+ _keys: Keys,
runtime: TradeAgreementAttestationRuntime,
policy: TradeAgreementAttestationPolicy,
mut stop_rx: watch::Receiver<bool>,
@@ -710,10 +700,10 @@ pub async fn subscriber(
subscribed_kinds
);
- let kinds: Vec<RadrootsNostrKind> = subscribed_kinds
+ let kinds: Vec<Kind> = subscribed_kinds
.iter()
.map(|kind| u16::try_from(*kind).expect("trade mutation kinds fit in nostr custom range"))
- .map(RadrootsNostrKind::Custom)
+ .map(Kind::Custom)
.collect();
let filter = runtime.recovery_filter(kinds).await;
@@ -742,7 +732,7 @@ pub async fn subscriber(
}
};
- if let RadrootsNostrRelayPoolNotification::Event { event, .. } = n {
+ if let RelayPoolNotification::Event { event, .. } = n {
let event = (*event).clone();
process_event_notification(event, runtime.clone(), policy.clone()).await?;
}
@@ -864,83 +854,10 @@ fn write_output(path: Option<&Path>, bytes: &[u8]) -> anyhow::Result<()> {
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{
- RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationPolicy,
- TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig,
- TradeAgreementAttestationSmokeOperation, TradeAgreementAttestationSmokeRequest,
- attest_projection_claim, handle_smoke_request_bytes,
- };
- use radroots_event::ids::{RadrootsTradeId, RadrootsTradeMutationId};
- use radroots_trade::workflow::{
- RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1,
- RadrootsTradeEvidenceStateV1, RadrootsTradeProjectionV1,
+ RHI_AGREEMENT_ATTESTATION_PROTOCOL_ID, TradeAgreementAttestationRuntime,
+ TradeAgreementAttestationRuntimeConfig, TradeAgreementAttestationSmokeOperation,
+ TradeAgreementAttestationSmokeRequest, handle_smoke_request_bytes,
};
-
- fn hex_64(ch: char) -> String {
- std::iter::repeat_n(ch, 64).collect()
- }
-
- fn hex_32(ch: char) -> String {
- std::iter::repeat_n(ch, 32).collect()
- }
-
- fn projection(claim_id: RadrootsTradeMutationId) -> RadrootsTradeProjectionV1 {
- RadrootsTradeProjectionV1 {
- reducer_contract_id: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_CONTRACT_ID
- .to_owned(),
- reducer_version: radroots_trade::workflow::RADROOTS_TRADE_REDUCER_VERSION,
- trade_id: RadrootsTradeId::parse(hex_32('1')).expect("trade id"),
- root_mutation_id: None,
- buyer_pubkey: None,
- seller_pubkey: None,
- farm_id: None,
- negotiation_state: Default::default(),
- agreement_state: RadrootsTradeAgreementStateV1::Agreed,
- evidence_state: RadrootsTradeEvidenceStateV1::Complete,
- conflict_state: Default::default(),
- private_terms_state: Default::default(),
- attestation_state: RadrootsTradeAttestationStateV1::None,
- fulfillment_state: Default::default(),
- payment_state: Default::default(),
- candidate_heads: Vec::new(),
- agreement_claims: vec![radroots_trade::workflow::RadrootsTradeAgreementClaimV1 {
- claim_mutation_id: claim_id.clone(),
- proposal_mutation_id: RadrootsTradeMutationId::parse(hex_64('2'))
- .expect("proposal id"),
- candidate_id: radroots_event::ids::RadrootsTradeCandidateId::parse(hex_64('3'))
- .expect("candidate id"),
- candidate_author_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('4'))
- .expect("author"),
- accepted_by_pubkey: radroots_event::ids::RadrootsPublicKey::parse(hex_64('5'))
- .expect("acceptor"),
- reservation_commitment: hex_64('6'),
- }],
- active_agreement_claim_ids: vec![claim_id],
- contested_claim_ids: Vec::new(),
- cancelled_claim_ids: Vec::new(),
- declined_candidate_ids: Vec::new(),
- missing_parent_ids: Vec::new(),
- missing_proposal_ids: Vec::new(),
- unsupported_mutation_ids: Vec::new(),
- issues: Vec::new(),
- attestations: Vec::new(),
- projection_digest: hex_64('7'),
- }
- }
-
- #[test]
- fn attestation_report_is_keyed_to_claim_and_projection_digest() {
- let claim_id = RadrootsTradeMutationId::parse(hex_64('a')).expect("claim id");
- let report = attest_projection_claim(
- &projection(claim_id.clone()),
- &claim_id,
- &TradeAgreementAttestationPolicy::default(),
- )
- .expect("report");
- assert_eq!(report.statement.claim_mutation_id, claim_id.as_str());
- assert_eq!(report.statement.projection_digest, hex_64('7'));
- assert_eq!(report.proof_system, "local_statement_hash");
- }
-
#[tokio::test]
async fn runtime_persists_and_loads_attestation_state() {
let temp = tempfile::tempdir().expect("tempdir");
diff --git a/src/host_identity.rs b/src/host_identity.rs
@@ -0,0 +1,327 @@
+//! Myc-owned secret identity container.
+//!
+//! `radroots_identity` deliberately exposes only public, transport-neutral
+//! values. This host-private type keeps service key custody and the legacy
+//! Nostr-facing profile payload inside Myc.
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use nostr::nips::nip19::ToBech32;
+use nostr::nips::nip49::{EncryptedSecretKey, KeySecurity};
+use nostr::{Keys, SecretKey};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum IdentityError {
+ #[error("identity file missing at {0}")]
+ NotFound(PathBuf),
+ #[error("identity generation is not permitted for {0}")]
+ GenerationNotAllowed(PathBuf),
+ #[error("failed to read identity file at {0}")]
+ Read(PathBuf, #[source] std::io::Error),
+ #[error("failed to create identity directory {0}")]
+ CreateDir(PathBuf, #[source] std::io::Error),
+ #[error("failed to write identity file at {0}")]
+ Write(PathBuf, #[source] std::io::Error),
+ #[error("invalid identity JSON")]
+ InvalidJson(#[from] serde_json::Error),
+ #[error("invalid secret key")]
+ InvalidSecretKey(#[from] nostr::key::Error),
+ #[error("invalid public key")]
+ InvalidPublicKey,
+ #[error("public key does not match secret key")]
+ PublicKeyMismatch,
+ #[error("invalid encrypted secret key")]
+ InvalidEncryptedSecretKey,
+ #[error("failed to encrypt secret key")]
+ EncryptSecretKey,
+ #[error("failed to decrypt encrypted secret key")]
+ DecryptEncryptedSecretKey,
+ #[error("unsupported identity file format")]
+ InvalidIdentityFormat,
+ #[error("protected identity storage error at {path}: {message}")]
+ ProtectedStorage { path: PathBuf, message: String },
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct RadrootsIdentityId(String);
+
+impl RadrootsIdentityId {
+ pub fn from_public_key(public_key: nostr::PublicKey) -> Result<Self, IdentityError> {
+ let key = radroots_nostr::key::public_key_from_nostr(public_key)
+ .map_err(|_| IdentityError::InvalidPublicKey)?;
+ Ok(Self(
+ radroots_identity::IdentityId::from_public_key(key).to_hex(),
+ ))
+ }
+
+ pub fn parse(value: &str) -> Result<Self, IdentityError> {
+ radroots_identity::IdentityId::from_hex(value)
+ .map(|identity_id| Self(identity_id.to_hex()))
+ .map_err(|_| IdentityError::InvalidPublicKey)
+ }
+
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+
+ pub fn into_string(self) -> String {
+ self.0
+ }
+
+ pub fn to_final(&self) -> radroots_identity::IdentityId {
+ radroots_identity::IdentityId::from_hex(self.0.as_str())
+ .expect("host identity ids are constructed from validated keys")
+ }
+}
+
+impl std::fmt::Display for RadrootsIdentityId {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(formatter)
+ }
+}
+
+impl From<radroots_identity::PublicKey> for RadrootsIdentityId {
+ fn from(public_key: radroots_identity::PublicKey) -> Self {
+ Self(radroots_identity::IdentityId::from_public_key(public_key).to_hex())
+ }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct RadrootsIdentityProfile {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub identifier: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub metadata: Option<nostr::Event>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub application_handler: Option<nostr::Event>,
+}
+
+impl RadrootsIdentityProfile {
+ pub fn is_empty(&self) -> bool {
+ self.identifier.is_none() && self.metadata.is_none() && self.application_handler.is_none()
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct RadrootsIdentityPublic {
+ pub id: RadrootsIdentityId,
+ pub public_key_hex: String,
+ pub public_key_npub: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub profile: Option<RadrootsIdentityProfile>,
+}
+
+impl PartialEq for RadrootsIdentityPublic {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.public_key_hex == other.public_key_hex
+ && self.profile == other.profile
+ }
+}
+
+impl Eq for RadrootsIdentityPublic {}
+
+impl RadrootsIdentityPublic {
+ pub fn new(public_key: nostr::PublicKey) -> Result<Self, IdentityError> {
+ Ok(Self {
+ id: RadrootsIdentityId::from_public_key(public_key)?,
+ public_key_hex: public_key.to_hex(),
+ public_key_npub: public_key
+ .to_bech32()
+ .expect("validated Nostr public keys encode as npub"),
+ profile: None,
+ })
+ }
+
+ pub fn with_profile(mut self, profile: RadrootsIdentityProfile) -> Self {
+ self.profile = (!profile.is_empty()).then_some(profile);
+ self
+ }
+
+ pub fn from_final_public_key(
+ public_key: radroots_identity::PublicKey,
+ ) -> Result<Self, IdentityError> {
+ let public_key = radroots_nostr::key::public_key_to_nostr(public_key)
+ .map_err(|_| IdentityError::InvalidPublicKey)?;
+ Self::new(public_key)
+ }
+
+ pub fn id(&self) -> &RadrootsIdentityId {
+ &self.id
+ }
+
+ pub fn public_key(&self) -> radroots_identity::PublicKey {
+ radroots_identity::PublicKey::from_hex(self.public_key_hex.as_str())
+ .expect("host public identities are constructed from validated keys")
+ }
+
+ pub fn to_final(&self) -> radroots_identity::PublicIdentity {
+ radroots_identity::PublicIdentity::new(self.public_key())
+ }
+
+ pub fn account_id(&self) -> radroots_identity::AccountId {
+ self.id.to_final().into()
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct RadrootsIdentityFile {
+ pub secret_key: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub public_key: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub identifier: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub metadata: Option<nostr::Event>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub application_handler: Option<nostr::Event>,
+}
+
+#[derive(Debug, Clone)]
+pub struct RadrootsIdentity {
+ keys: Keys,
+ profile: Option<RadrootsIdentityProfile>,
+}
+
+impl RadrootsIdentity {
+ pub fn new(keys: Keys) -> Self {
+ Self {
+ keys,
+ profile: None,
+ }
+ }
+
+ pub fn generate() -> Self {
+ Self::new(Keys::generate())
+ }
+
+ pub fn from_secret_key_str(value: &str) -> Result<Self, IdentityError> {
+ let secret = SecretKey::parse(value)?;
+ Ok(Self::new(Keys::new(secret)))
+ }
+
+ pub fn from_encrypted_secret_key_str(
+ payload: &str,
+ password: &str,
+ ) -> Result<Self, IdentityError> {
+ use nostr::nips::nip19::FromBech32;
+ let encrypted = EncryptedSecretKey::from_bech32(payload)
+ .map_err(|_| IdentityError::InvalidEncryptedSecretKey)?;
+ let secret = encrypted
+ .decrypt(password)
+ .map_err(|_| IdentityError::DecryptEncryptedSecretKey)?;
+ Ok(Self::new(Keys::new(secret)))
+ }
+
+ pub fn encrypt_secret_key_ncryptsec(&self, password: &str) -> Result<String, IdentityError> {
+ let encrypted =
+ EncryptedSecretKey::new(self.keys.secret_key(), password, 16, KeySecurity::Unknown)
+ .map_err(|_| IdentityError::EncryptSecretKey)?;
+ encrypted
+ .to_bech32()
+ .map_err(|_| IdentityError::EncryptSecretKey)
+ }
+
+ pub fn keys(&self) -> &Keys {
+ &self.keys
+ }
+
+ pub fn public_key(&self) -> nostr::PublicKey {
+ self.keys.public_key()
+ }
+
+ pub fn final_public_key(&self) -> radroots_identity::PublicKey {
+ radroots_nostr::key::public_key_from_nostr(self.public_key())
+ .expect("identity keys always contain a valid public key")
+ }
+
+ pub fn id(&self) -> RadrootsIdentityId {
+ RadrootsIdentityId::from_public_key(self.public_key())
+ .expect("identity keys always contain a valid public key")
+ }
+
+ pub fn public_key_hex(&self) -> String {
+ self.public_key().to_hex()
+ }
+
+ pub fn secret_key_hex(&self) -> String {
+ self.keys.secret_key().to_secret_hex()
+ }
+
+ pub fn profile(&self) -> Option<&RadrootsIdentityProfile> {
+ self.profile.as_ref()
+ }
+
+ pub fn set_profile(&mut self, profile: RadrootsIdentityProfile) {
+ self.profile = (!profile.is_empty()).then_some(profile);
+ }
+
+ pub fn to_public(&self) -> RadrootsIdentityPublic {
+ let mut public = RadrootsIdentityPublic::new(self.public_key())
+ .expect("identity keys always contain a valid public key");
+ public.profile = self.profile.clone();
+ public
+ }
+
+ pub fn to_file(&self) -> RadrootsIdentityFile {
+ let profile = self.profile.clone().unwrap_or_default();
+ RadrootsIdentityFile {
+ secret_key: self.secret_key_hex(),
+ public_key: Some(self.public_key_hex()),
+ identifier: profile.identifier,
+ metadata: profile.metadata,
+ application_handler: profile.application_handler,
+ }
+ }
+
+ pub fn save_json(&self, path: impl AsRef<Path>) -> Result<(), IdentityError> {
+ let path = path.as_ref();
+ if let Some(parent) = path.parent().filter(|value| !value.as_os_str().is_empty()) {
+ fs::create_dir_all(parent)
+ .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?;
+ }
+ fs::write(path, serde_json::to_vec_pretty(&self.to_file())?)
+ .map_err(|source| IdentityError::Write(path.to_path_buf(), source))
+ }
+
+ pub fn load_from_path_auto(path: impl AsRef<Path>) -> Result<Self, IdentityError> {
+ let path = path.as_ref();
+ let encoded = fs::read(path).map_err(|source| {
+ if source.kind() == std::io::ErrorKind::NotFound {
+ IdentityError::NotFound(path.to_path_buf())
+ } else {
+ IdentityError::Read(path.to_path_buf(), source)
+ }
+ })?;
+ let file: RadrootsIdentityFile = serde_json::from_slice(encoded.as_slice())?;
+ Self::try_from(file)
+ }
+}
+
+impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity {
+ type Error = IdentityError;
+
+ fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> {
+ let mut identity = Self::from_secret_key_str(file.secret_key.as_str())?;
+ if file
+ .public_key
+ .as_deref()
+ .is_some_and(|public| public != identity.public_key_hex())
+ {
+ return Err(IdentityError::PublicKeyMismatch);
+ }
+ identity.set_profile(RadrootsIdentityProfile {
+ identifier: file.identifier,
+ metadata: file.metadata,
+ application_handler: file.application_handler,
+ });
+ Ok(identity)
+ }
+}
diff --git a/src/host_nostr.rs b/src/host_nostr.rs
@@ -0,0 +1,53 @@
+//! RHI-owned relay client over the final portable Nostr contract.
+
+use core::time::Duration;
+
+pub use nostr::{Event, Filter, Keys, Kind, Metadata, SubscriptionId, Timestamp};
+pub use nostr::{Tag, TagKind};
+pub use nostr_sdk::RelayPoolNotification;
+pub use radroots_nostr::event::{ApplicationHandlerSpec, GenericBuilder, ProfileBuilder};
+
+#[derive(Clone)]
+pub struct Client {
+ inner: nostr_sdk::Client,
+ keys: Keys,
+}
+
+impl Client {
+ pub fn new(keys: Keys) -> Self {
+ let inner = nostr_sdk::Client::new(keys.clone());
+ inner.automatic_authentication(false);
+ Self { inner, keys }
+ }
+
+ pub fn into_inner(self) -> nostr_sdk::Client {
+ self.inner
+ }
+ pub fn keys(&self) -> &Keys {
+ &self.keys
+ }
+ pub async fn connect(&self) {
+ self.inner.connect().await;
+ }
+ pub async fn wait_for_connection(&self, timeout: Duration) {
+ self.inner.wait_for_connection(timeout).await;
+ }
+ pub async fn add_relay(&self, url: &str) -> Result<bool, nostr_sdk::client::Error> {
+ self.inner.add_relay(url).await
+ }
+ pub async fn subscribe(
+ &self,
+ filter: Filter,
+ ) -> Result<SubscriptionId, nostr_sdk::client::Error> {
+ Ok(self.inner.subscribe(filter, None).await?.val)
+ }
+ pub async fn unsubscribe(&self, id: &SubscriptionId) {
+ self.inner.unsubscribe(id).await;
+ }
+ pub async fn send_event(
+ &self,
+ event: &Event,
+ ) -> Result<nostr_sdk::prelude::Output<nostr::EventId>, nostr_sdk::client::Error> {
+ self.inner.send_event(event).await
+ }
+}
diff --git a/src/host_paths/error.rs b/src/host_paths/error.rs
@@ -0,0 +1,35 @@
+use thiserror::Error;
+
+use std::path::PathBuf;
+
+use super::{RadrootsPathProfile, RadrootsPlatform};
+
+#[derive(Debug, Error, Clone, PartialEq, Eq)]
+pub enum RadrootsRuntimePathsError {
+ #[error("interactive_user on {platform} requires a home directory")]
+ MissingHomeDir { platform: RadrootsPlatform },
+
+ #[error("interactive_user on windows requires APPDATA and LOCALAPPDATA roots")]
+ MissingWindowsUserDirs,
+
+ #[error("service_host on windows requires a ProgramData root")]
+ MissingWindowsProgramDataDir,
+
+ #[error("repo_local requires an explicit repo-local base root")]
+ MissingRepoLocalRoot,
+
+ #[error("mobile_native requires explicit logical roots")]
+ MissingMobileRoots,
+
+ #[error("{profile} is not supported on {platform}")]
+ UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile,
+ platform: RadrootsPlatform,
+ },
+
+ #[error("runtime namespace `{value}` must be one non-empty path component")]
+ InvalidNamespaceComponent { value: String },
+
+ #[error("shared accounts data root `{path:?}` has no parent shared data root")]
+ SharedAccountsDataRootMissingParent { path: PathBuf },
+}
diff --git a/src/host_paths/mod.rs b/src/host_paths/mod.rs
@@ -0,0 +1,11 @@
+//! RHI-owned host path policy.
+
+mod error;
+mod namespace;
+mod platform;
+mod roots;
+
+pub use error::RadrootsRuntimePathsError;
+pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
+pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
+pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths};
diff --git a/src/host_paths/namespace.rs b/src/host_paths/namespace.rs
@@ -0,0 +1,148 @@
+use std::path::PathBuf;
+
+use super::RadrootsRuntimePathsError;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsRuntimeNamespaceKind {
+ App,
+ Service,
+ Worker,
+ Shared,
+}
+
+impl RadrootsRuntimeNamespaceKind {
+ #[must_use]
+ pub fn path_segment(self) -> &'static str {
+ match self {
+ Self::App => "apps",
+ Self::Service => "services",
+ Self::Worker => "workers",
+ Self::Shared => "shared",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsRuntimeNamespace {
+ kind: RadrootsRuntimeNamespaceKind,
+ value: String,
+}
+
+impl RadrootsRuntimeNamespace {
+ pub fn app(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
+ Self::new(RadrootsRuntimeNamespaceKind::App, value)
+ }
+
+ pub fn service(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
+ Self::new(RadrootsRuntimeNamespaceKind::Service, value)
+ }
+
+ pub fn worker(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
+ Self::new(RadrootsRuntimeNamespaceKind::Worker, value)
+ }
+
+ pub fn shared(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> {
+ Self::new(RadrootsRuntimeNamespaceKind::Shared, value)
+ }
+
+ pub fn new(
+ kind: RadrootsRuntimeNamespaceKind,
+ value: impl Into<String>,
+ ) -> Result<Self, RadrootsRuntimePathsError> {
+ let value = value.into();
+ validate_component(&value)?;
+ Ok(Self { kind, value })
+ }
+
+ #[must_use]
+ pub fn kind(&self) -> RadrootsRuntimeNamespaceKind {
+ self.kind
+ }
+
+ #[must_use]
+ pub fn value(&self) -> &str {
+ self.value.as_str()
+ }
+
+ #[must_use]
+ pub fn relative_path(&self) -> PathBuf {
+ PathBuf::from(self.kind.path_segment()).join(self.value.as_str())
+ }
+}
+
+fn validate_component(value: &str) -> Result<(), RadrootsRuntimePathsError> {
+ let trimmed = value.trim();
+ if trimmed.is_empty()
+ || trimmed == "."
+ || trimmed == ".."
+ || trimmed.contains('/')
+ || trimmed.contains('\\')
+ {
+ return Err(RadrootsRuntimePathsError::InvalidNamespaceComponent {
+ value: value.to_owned(),
+ });
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::RadrootsRuntimePathsError;
+ use super::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
+
+ #[test]
+ fn namespace_kind_path_segments_are_canonical() {
+ assert_eq!(RadrootsRuntimeNamespaceKind::App.path_segment(), "apps");
+ assert_eq!(
+ RadrootsRuntimeNamespaceKind::Service.path_segment(),
+ "services"
+ );
+ assert_eq!(
+ RadrootsRuntimeNamespaceKind::Worker.path_segment(),
+ "workers"
+ );
+ assert_eq!(
+ RadrootsRuntimeNamespaceKind::Shared.path_segment(),
+ "shared"
+ );
+ }
+
+ #[test]
+ fn namespace_constructors_preserve_kind_and_value() {
+ let app = RadrootsRuntimeNamespace::app("cli").expect("app namespace");
+ assert_eq!(app.kind(), RadrootsRuntimeNamespaceKind::App);
+ assert_eq!(app.value(), "cli");
+ assert_eq!(app.relative_path(), PathBuf::from("apps/cli"));
+
+ let service = RadrootsRuntimeNamespace::service("myc").expect("service namespace");
+ assert_eq!(service.kind(), RadrootsRuntimeNamespaceKind::Service);
+ assert_eq!(service.value(), "myc");
+ assert_eq!(service.relative_path(), PathBuf::from("services/myc"));
+
+ let worker = RadrootsRuntimeNamespace::worker("rhi").expect("worker namespace");
+ assert_eq!(worker.kind(), RadrootsRuntimeNamespaceKind::Worker);
+ assert_eq!(worker.value(), "rhi");
+ assert_eq!(worker.relative_path(), PathBuf::from("workers/rhi"));
+
+ let shared = RadrootsRuntimeNamespace::shared("runtime").expect("shared namespace");
+ assert_eq!(shared.kind(), RadrootsRuntimeNamespaceKind::Shared);
+ assert_eq!(shared.value(), "runtime");
+ assert_eq!(shared.relative_path(), PathBuf::from("shared/runtime"));
+ }
+
+ #[test]
+ fn namespace_validation_rejects_invalid_components() {
+ for invalid in ["", " ", ".", "..", "a/b", r"a\b"] {
+ let err = RadrootsRuntimeNamespace::new(RadrootsRuntimeNamespaceKind::App, invalid)
+ .expect_err("invalid namespace component should fail");
+ assert_eq!(
+ err,
+ RadrootsRuntimePathsError::InvalidNamespaceComponent {
+ value: invalid.to_owned(),
+ }
+ );
+ }
+ }
+}
diff --git a/src/host_paths/platform.rs b/src/host_paths/platform.rs
@@ -0,0 +1,179 @@
+use std::fmt;
+use std::path::PathBuf;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsPlatform {
+ Linux,
+ Macos,
+ Windows,
+ Android,
+ Ios,
+}
+
+impl RadrootsPlatform {
+ #[must_use]
+ #[cfg(target_os = "android")]
+ pub fn current() -> Self {
+ Self::Android
+ }
+
+ #[must_use]
+ #[cfg(target_os = "ios")]
+ pub fn current() -> Self {
+ Self::Ios
+ }
+
+ #[must_use]
+ #[cfg(target_os = "macos")]
+ pub fn current() -> Self {
+ Self::Macos
+ }
+
+ #[must_use]
+ #[cfg(target_os = "windows")]
+ pub fn current() -> Self {
+ Self::Windows
+ }
+
+ #[must_use]
+ #[cfg(all(
+ not(target_os = "android"),
+ not(target_os = "ios"),
+ not(target_os = "macos"),
+ not(target_os = "windows")
+ ))]
+ pub fn current() -> Self {
+ Self::Linux
+ }
+
+ #[must_use]
+ pub fn is_unix_like(self) -> bool {
+ matches!(self, Self::Linux | Self::Macos)
+ }
+}
+
+impl fmt::Display for RadrootsPlatform {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ Self::Linux => "linux",
+ Self::Macos => "macos",
+ Self::Windows => "windows",
+ Self::Android => "android",
+ Self::Ios => "ios",
+ })
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RadrootsPathProfile {
+ InteractiveUser,
+ ServiceHost,
+ RepoLocal,
+ MobileNative,
+}
+
+impl fmt::Display for RadrootsPathProfile {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(match self {
+ Self::InteractiveUser => "interactive_user",
+ Self::ServiceHost => "service_host",
+ Self::RepoLocal => "repo_local",
+ Self::MobileNative => "mobile_native",
+ })
+ }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct RadrootsHostEnvironment {
+ pub home_dir: Option<PathBuf>,
+ pub appdata_dir: Option<PathBuf>,
+ pub localappdata_dir: Option<PathBuf>,
+ pub programdata_dir: Option<PathBuf>,
+}
+
+impl RadrootsHostEnvironment {
+ #[must_use]
+ pub fn from_current_process() -> Self {
+ Self {
+ home_dir: std::env::var_os("HOME").map(PathBuf::from),
+ appdata_dir: std::env::var_os("APPDATA").map(PathBuf::from),
+ localappdata_dir: std::env::var_os("LOCALAPPDATA").map(PathBuf::from),
+ programdata_dir: std::env::var_os("ProgramData").map(PathBuf::from),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
+
+ #[test]
+ fn current_matches_compiled_target_platform() {
+ #[cfg(target_os = "android")]
+ let expected = RadrootsPlatform::Android;
+ #[cfg(target_os = "ios")]
+ let expected = RadrootsPlatform::Ios;
+ #[cfg(target_os = "macos")]
+ let expected = RadrootsPlatform::Macos;
+ #[cfg(target_os = "windows")]
+ let expected = RadrootsPlatform::Windows;
+ #[cfg(all(
+ not(target_os = "android"),
+ not(target_os = "ios"),
+ not(target_os = "macos"),
+ not(target_os = "windows")
+ ))]
+ let expected = RadrootsPlatform::Linux;
+
+ assert_eq!(RadrootsPlatform::current(), expected);
+ }
+
+ #[test]
+ fn unix_like_classification_is_explicit() {
+ assert!(RadrootsPlatform::Linux.is_unix_like());
+ assert!(RadrootsPlatform::Macos.is_unix_like());
+ assert!(!RadrootsPlatform::Windows.is_unix_like());
+ assert!(!RadrootsPlatform::Android.is_unix_like());
+ assert!(!RadrootsPlatform::Ios.is_unix_like());
+ }
+
+ #[test]
+ fn display_uses_canonical_labels() {
+ assert_eq!(RadrootsPlatform::Linux.to_string(), "linux");
+ assert_eq!(RadrootsPlatform::Macos.to_string(), "macos");
+ assert_eq!(RadrootsPlatform::Windows.to_string(), "windows");
+ assert_eq!(RadrootsPlatform::Android.to_string(), "android");
+ assert_eq!(RadrootsPlatform::Ios.to_string(), "ios");
+
+ assert_eq!(
+ RadrootsPathProfile::InteractiveUser.to_string(),
+ "interactive_user"
+ );
+ assert_eq!(RadrootsPathProfile::ServiceHost.to_string(), "service_host");
+ assert_eq!(RadrootsPathProfile::RepoLocal.to_string(), "repo_local");
+ assert_eq!(
+ RadrootsPathProfile::MobileNative.to_string(),
+ "mobile_native"
+ );
+ }
+
+ #[test]
+ fn host_environment_reads_current_process_variables() {
+ let env = RadrootsHostEnvironment::from_current_process();
+ assert_eq!(env.home_dir, std::env::var_os("HOME").map(PathBuf::from));
+ assert_eq!(
+ env.appdata_dir,
+ std::env::var_os("APPDATA").map(PathBuf::from)
+ );
+ assert_eq!(
+ env.localappdata_dir,
+ std::env::var_os("LOCALAPPDATA").map(PathBuf::from)
+ );
+ assert_eq!(
+ env.programdata_dir,
+ std::env::var_os("ProgramData").map(PathBuf::from)
+ );
+ }
+}
diff --git a/src/host_paths/roots.rs b/src/host_paths/roots.rs
@@ -0,0 +1,315 @@
+use std::path::{Path, PathBuf};
+
+use super::{
+ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimeNamespace,
+ RadrootsRuntimePathsError,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsPaths {
+ pub config: PathBuf,
+ pub data: PathBuf,
+ pub cache: PathBuf,
+ pub logs: PathBuf,
+ pub run: PathBuf,
+ pub secrets: PathBuf,
+}
+
+impl RadrootsPaths {
+ #[must_use]
+ pub fn from_base_root(base_root: impl AsRef<Path>) -> Self {
+ let base_root = base_root.as_ref();
+ Self {
+ config: base_root.join("config"),
+ data: base_root.join("data"),
+ cache: base_root.join("cache"),
+ logs: base_root.join("logs"),
+ run: base_root.join("run"),
+ secrets: base_root.join("secrets"),
+ }
+ }
+
+ #[must_use]
+ pub fn namespaced(&self, namespace: &RadrootsRuntimeNamespace) -> Self {
+ let relative = namespace.relative_path();
+ Self {
+ config: self.config.join(&relative),
+ data: self.data.join(&relative),
+ cache: self.cache.join(&relative),
+ logs: self.logs.join(&relative),
+ run: self.run.join(&relative),
+ secrets: self.secrets.join(relative),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct RadrootsPathOverrides {
+ pub repo_local_root: Option<PathBuf>,
+ pub mobile_roots: Option<RadrootsPaths>,
+}
+
+impl RadrootsPathOverrides {
+ #[must_use]
+ pub fn repo_local(base_root: impl Into<PathBuf>) -> Self {
+ Self {
+ repo_local_root: Some(base_root.into()),
+ mobile_roots: None,
+ }
+ }
+
+ #[must_use]
+ pub fn mobile(roots: RadrootsPaths) -> Self {
+ Self {
+ repo_local_root: None,
+ mobile_roots: Some(roots),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsPathResolver {
+ platform: RadrootsPlatform,
+ host_environment: RadrootsHostEnvironment,
+}
+
+impl RadrootsPathResolver {
+ #[must_use]
+ pub fn new(platform: RadrootsPlatform, host_environment: RadrootsHostEnvironment) -> Self {
+ Self {
+ platform,
+ host_environment,
+ }
+ }
+
+ #[must_use]
+ pub fn current() -> Self {
+ Self::new(
+ RadrootsPlatform::current(),
+ RadrootsHostEnvironment::from_current_process(),
+ )
+ }
+
+ #[must_use]
+ pub fn platform(&self) -> RadrootsPlatform {
+ self.platform
+ }
+
+ pub fn resolve(
+ &self,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+ ) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
+ match profile {
+ RadrootsPathProfile::InteractiveUser => self.resolve_interactive_user(),
+ RadrootsPathProfile::ServiceHost => self.resolve_service_host(),
+ RadrootsPathProfile::RepoLocal => overrides
+ .repo_local_root
+ .as_ref()
+ .map(RadrootsPaths::from_base_root)
+ .ok_or(RadrootsRuntimePathsError::MissingRepoLocalRoot),
+ RadrootsPathProfile::MobileNative => match self.platform {
+ RadrootsPlatform::Android | RadrootsPlatform::Ios => overrides
+ .mobile_roots
+ .clone()
+ .ok_or(RadrootsRuntimePathsError::MissingMobileRoots),
+ _ => Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile,
+ platform: self.platform,
+ }),
+ },
+ }
+ }
+
+ fn resolve_interactive_user(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
+ match self.platform {
+ RadrootsPlatform::Linux | RadrootsPlatform::Macos => self
+ .host_environment
+ .home_dir
+ .as_ref()
+ .map(|home| RadrootsPaths::from_base_root(home.join(".radroots")))
+ .ok_or(RadrootsRuntimePathsError::MissingHomeDir {
+ platform: self.platform,
+ }),
+ RadrootsPlatform::Windows => {
+ let appdata = self
+ .host_environment
+ .appdata_dir
+ .as_ref()
+ .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?;
+ let localappdata = self
+ .host_environment
+ .localappdata_dir
+ .as_ref()
+ .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?;
+ let config_root = appdata.join("Radroots");
+ let local_root = localappdata.join("Radroots");
+ Ok(RadrootsPaths {
+ config: config_root.join("config"),
+ data: local_root.join("data"),
+ cache: local_root.join("cache"),
+ logs: local_root.join("logs"),
+ run: local_root.join("run"),
+ secrets: config_root.join("secrets"),
+ })
+ }
+ RadrootsPlatform::Android | RadrootsPlatform::Ios => {
+ Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile::InteractiveUser,
+ platform: self.platform,
+ })
+ }
+ }
+ }
+
+ fn resolve_service_host(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> {
+ match self.platform {
+ RadrootsPlatform::Windows => {
+ let programdata = self
+ .host_environment
+ .programdata_dir
+ .as_ref()
+ .ok_or(RadrootsRuntimePathsError::MissingWindowsProgramDataDir)?;
+ let base = programdata.join("Radroots");
+ Ok(RadrootsPaths {
+ config: base.join("config"),
+ data: base.join("data"),
+ cache: base.join("cache"),
+ logs: base.join("logs"),
+ run: base.join("run"),
+ secrets: base.join("secrets"),
+ })
+ }
+ RadrootsPlatform::Linux | RadrootsPlatform::Macos => Ok(RadrootsPaths {
+ config: PathBuf::from("/etc/radroots"),
+ data: PathBuf::from("/var/lib/radroots"),
+ cache: PathBuf::from("/var/cache/radroots"),
+ logs: PathBuf::from("/var/log/radroots"),
+ run: PathBuf::from("/run/radroots"),
+ secrets: PathBuf::from("/etc/radroots/secrets"),
+ }),
+ RadrootsPlatform::Android | RadrootsPlatform::Ios => {
+ Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile::ServiceHost,
+ platform: self.platform,
+ })
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::{
+ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimePathsError,
+ };
+ use super::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths};
+
+ #[test]
+ fn path_override_helpers_only_populate_their_owned_slot() {
+ let repo_local = RadrootsPathOverrides::repo_local("/repo/.local/radroots");
+ assert_eq!(
+ repo_local.repo_local_root,
+ Some(PathBuf::from("/repo/.local/radroots"))
+ );
+ assert!(repo_local.mobile_roots.is_none());
+
+ let mobile_roots = RadrootsPaths::from_base_root("/sandbox");
+ let mobile = RadrootsPathOverrides::mobile(mobile_roots.clone());
+ assert!(mobile.repo_local_root.is_none());
+ assert_eq!(mobile.mobile_roots, Some(mobile_roots));
+ }
+
+ #[test]
+ fn resolver_current_uses_process_platform_and_environment() {
+ let resolver = RadrootsPathResolver::current();
+ assert_eq!(resolver.platform(), RadrootsPlatform::current());
+ assert_eq!(
+ resolver,
+ RadrootsPathResolver::new(
+ RadrootsPlatform::current(),
+ RadrootsHostEnvironment::from_current_process()
+ )
+ );
+ }
+
+ #[test]
+ fn mobile_profile_is_rejected_on_non_mobile_platforms() {
+ let resolver =
+ RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
+
+ let err = resolver
+ .resolve(
+ RadrootsPathProfile::MobileNative,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect_err("mobile profile should be rejected on linux");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile::MobileNative,
+ platform: RadrootsPlatform::Linux,
+ }
+ );
+ }
+
+ #[test]
+ fn interactive_user_is_rejected_on_mobile_platforms() {
+ for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] {
+ let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
+ let err = resolver
+ .resolve(
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect_err("interactive_user should be unsupported on mobile");
+ assert_eq!(
+ err,
+ RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile::InteractiveUser,
+ platform,
+ }
+ );
+ }
+ }
+
+ #[test]
+ fn service_host_windows_requires_programdata() {
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Windows,
+ RadrootsHostEnvironment::default(),
+ );
+
+ let err = resolver
+ .resolve(
+ RadrootsPathProfile::ServiceHost,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect_err("service_host on windows should require programdata");
+
+ assert_eq!(err, RadrootsRuntimePathsError::MissingWindowsProgramDataDir);
+ }
+
+ #[test]
+ fn service_host_is_rejected_on_mobile_platforms() {
+ for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] {
+ let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
+ let err = resolver
+ .resolve(
+ RadrootsPathProfile::ServiceHost,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect_err("service_host should be unsupported on mobile");
+ assert_eq!(
+ err,
+ RadrootsRuntimePathsError::UnsupportedProfilePlatform {
+ profile: RadrootsPathProfile::ServiceHost,
+ platform,
+ }
+ );
+ }
+ }
+}
diff --git a/src/host_runtime.rs b/src/host_runtime.rs
@@ -0,0 +1,131 @@
+//! RHI-owned process lifecycle and retry policy.
+
+use core::future::Future;
+use core::time::Duration;
+use std::path::PathBuf;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use clap::{ArgAction, Args, ValueHint};
+use serde::{Deserialize, Serialize};
+
+#[derive(Args, Debug, Clone)]
+pub struct ServiceCliArgs {
+ #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
+ pub config: Option<PathBuf>,
+ #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
+ pub identity: Option<PathBuf>,
+ #[arg(long, action = ArgAction::SetTrue)]
+ pub allow_generate_identity: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct NostrServiceConfig {
+ pub logs_dir: String,
+ #[serde(default)]
+ pub relays: Vec<String>,
+ #[serde(default)]
+ pub nip89_identifier: Option<String>,
+ #[serde(default)]
+ pub nip89_extra_tags: Vec<Vec<String>>,
+}
+
+const fn default_base_ms() -> u64 {
+ 500
+}
+const fn default_max_ms() -> u64 {
+ 30_000
+}
+const fn default_factor() -> u32 {
+ 2
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default, deny_unknown_fields)]
+pub struct BackoffConfig {
+ #[serde(default = "default_base_ms")]
+ pub base_ms: u64,
+ #[serde(default = "default_max_ms")]
+ pub max_ms: u64,
+ #[serde(default = "default_factor")]
+ pub factor: u32,
+ #[serde(default)]
+ pub jitter_ms: u64,
+}
+
+impl Default for BackoffConfig {
+ fn default() -> Self {
+ Self {
+ base_ms: default_base_ms(),
+ max_ms: default_max_ms(),
+ factor: default_factor(),
+ jitter_ms: 0,
+ }
+ }
+}
+
+impl BackoffConfig {
+ fn delay_for_attempt(&self, attempt: u32) -> Duration {
+ let base = self.base_ms.max(1);
+ let max = self.max_ms.max(base);
+ let factor = u64::from(self.factor.max(1));
+ let mut delay = base;
+ for _ in 0..attempt.saturating_sub(1).min(10) {
+ delay = delay.saturating_mul(factor).min(max);
+ }
+ if self.jitter_ms > 0 {
+ let nanos = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .subsec_nanos();
+ delay = delay
+ .saturating_add(u64::from(nanos) % (self.jitter_ms + 1))
+ .min(max);
+ }
+ Duration::from_millis(delay)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Backoff {
+ config: BackoffConfig,
+ attempt: u32,
+}
+
+impl Backoff {
+ pub fn new(config: BackoffConfig) -> Self {
+ Self { config, attempt: 0 }
+ }
+ pub fn reset(&mut self) {
+ self.attempt = 0;
+ }
+ pub fn next_delay(&mut self) -> Duration {
+ self.attempt = self.attempt.saturating_add(1);
+ self.config.delay_for_attempt(self.attempt)
+ }
+}
+
+pub async fn shutdown_signal() {
+ let ctrl_c = async {
+ tokio::signal::ctrl_c()
+ .await
+ .expect("install Ctrl+C handler")
+ };
+ #[cfg(unix)]
+ let terminate = async {
+ tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
+ .expect("install termination handler")
+ .recv()
+ .await;
+ };
+ #[cfg(not(unix))]
+ let terminate = core::future::pending::<()>();
+ wait_for_shutdown(ctrl_c, terminate).await;
+}
+
+async fn wait_for_shutdown<C, T>(ctrl_c: C, terminate: T)
+where
+ C: Future<Output = ()>,
+ T: Future<Output = ()>,
+{
+ tokio::select! { _ = ctrl_c => {}, _ = terminate => {} }
+}
diff --git a/src/identity_storage.rs b/src/identity_storage.rs
@@ -1,78 +1,364 @@
+use std::ffi::OsString;
+use std::fs::{self, OpenOptions};
+use std::io::Write;
use std::path::{Path, PathBuf};
-use anyhow::Result;
-use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityFile};
+use chacha20poly1305::aead::{Aead, KeyInit, Payload};
+use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
+use radroots_secrets::envelope::{Nonce, SealMaterial, SealRequest};
+use radroots_secrets::error::Operation;
+use radroots_secrets::id::{BackendKind, KeyVersion};
+use radroots_secrets::wrapping::{
+ BoxFuture, SecretMaterial, UnwrapRequest, WrapRequest, WrappedSecret,
+};
+use radroots_secrets::{EncryptedEnvelope, KeyWrapping, SecretId, SecretRef};
+use zeroize::Zeroize;
+
+use crate::host_identity::{
+ IdentityError, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityPublic,
+};
const RHI_IDENTITY_KEY_SLOT: &str = "rhi_identity";
+const WRAPPING_KEY_BYTES: usize = 32;
+const WRAPPING_NONCE_BYTES: usize = 24;
+const WRAPPED_KEY_VERSION: u8 = 1;
-#[cfg(test)]
pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf {
- radroots_runtime::local_wrapping_key_path(path)
+ encrypted_identity_wrapping_key_path(path)
}
pub fn load_service_identity(
path: Option<&Path>,
allow_generate: bool,
-) -> Result<RadrootsIdentity> {
- let path = resolved_identity_path(path);
+) -> Result<RadrootsIdentity, IdentityError> {
+ let path = path.map(Path::to_path_buf).unwrap_or_else(|| {
+ crate::paths::default_identity_path_for_process()
+ .expect("resolve canonical rhi identity path")
+ });
if path.exists() {
- return load_encrypted_identity(&path);
+ return load_encrypted_identity(path);
}
if !allow_generate {
- return Err(IdentityError::GenerationNotAllowed(path).into());
+ return Err(IdentityError::GenerationNotAllowed(path));
}
-
let identity = RadrootsIdentity::generate();
- store_encrypted_identity(&path, &identity)?;
+ store_encrypted_identity(path, &identity)?;
Ok(identity)
}
-pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &RadrootsIdentity) -> Result<()> {
+struct RhiFileKeyWrapping {
+ key_path: PathBuf,
+}
+
+impl RhiFileKeyWrapping {
+ fn new(identity_path: &Path) -> Self {
+ Self {
+ key_path: encrypted_identity_wrapping_key_path(identity_path),
+ }
+ }
+
+ fn load_or_create_key(&self) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> {
+ if let Ok(raw) = fs::read(&self.key_path) {
+ return key_from_bytes(raw.as_slice());
+ }
+ if let Some(parent) = self
+ .key_path
+ .parent()
+ .filter(|path| !path.as_os_str().is_empty())
+ {
+ fs::create_dir_all(parent).map_err(|_| secret_backend_failure(Operation::Provision))?;
+ }
+ let key: [u8; WRAPPING_KEY_BYTES] = rand::random();
+ match OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(&self.key_path)
+ {
+ Ok(mut file) => {
+ file.write_all(&key)
+ .map_err(|_| secret_backend_failure(Operation::Write))?;
+ file.sync_all()
+ .map_err(|_| secret_backend_failure(Operation::Write))?;
+ set_secret_permissions(&self.key_path)
+ .map_err(|_| secret_backend_failure(Operation::Write))?;
+ Ok(key)
+ }
+ Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
+ let raw = fs::read(&self.key_path)
+ .map_err(|_| secret_backend_failure(Operation::Read))?;
+ key_from_bytes(raw.as_slice())
+ }
+ Err(_) => Err(secret_backend_failure(Operation::Provision)),
+ }
+ }
+
+ fn load_key(&self) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> {
+ let raw = fs::read(&self.key_path).map_err(|_| secret_backend_failure(Operation::Read))?;
+ key_from_bytes(raw.as_slice())
+ }
+}
+
+impl KeyWrapping for RhiFileKeyWrapping {
+ fn wrap<'a>(
+ &'a self,
+ request: WrapRequest<'a>,
+ ) -> BoxFuture<'a, Result<WrappedSecret, radroots_secrets::Error>> {
+ Box::pin(async move {
+ let mut key = self.load_or_create_key()?;
+ let nonce: [u8; WRAPPING_NONCE_BYTES] = rand::random();
+ let ciphertext = request.plaintext().expose_secret(|plaintext| {
+ XChaCha20Poly1305::new(Key::from_slice(&key)).encrypt(
+ XNonce::from_slice(&nonce),
+ Payload {
+ msg: plaintext,
+ aad: request.reference().id().as_str().as_bytes(),
+ },
+ )
+ });
+ key.zeroize();
+ let ciphertext = ciphertext.map_err(|_| secret_backend_failure(Operation::Wrap))?;
+ let mut wrapped = Vec::with_capacity(1 + nonce.len() + ciphertext.len());
+ wrapped.push(WRAPPED_KEY_VERSION);
+ wrapped.extend_from_slice(&nonce);
+ wrapped.extend_from_slice(ciphertext.as_slice());
+ WrappedSecret::from_bytes(wrapped)
+ })
+ }
+
+ fn unwrap<'a>(
+ &'a self,
+ request: UnwrapRequest<'a>,
+ ) -> BoxFuture<'a, Result<SecretMaterial, radroots_secrets::Error>> {
+ Box::pin(async move {
+ let wrapped = request.wrapped().as_bytes();
+ if wrapped.len() <= 1 + WRAPPING_NONCE_BYTES || wrapped[0] != WRAPPED_KEY_VERSION {
+ return Err(secret_backend_failure(Operation::Unwrap));
+ }
+ let mut key = self.load_key()?;
+ let plaintext = XChaCha20Poly1305::new(Key::from_slice(&key)).decrypt(
+ XNonce::from_slice(&wrapped[1..1 + WRAPPING_NONCE_BYTES]),
+ Payload {
+ msg: &wrapped[1 + WRAPPING_NONCE_BYTES..],
+ aad: request.reference().id().as_str().as_bytes(),
+ },
+ );
+ key.zeroize();
+ SecretMaterial::from_slice(
+ &plaintext.map_err(|_| secret_backend_failure(Operation::Unwrap))?,
+ )
+ })
+ }
+}
+
+fn identity_secret_ref() -> Result<SecretRef, radroots_secrets::Error> {
+ Ok(SecretRef::new(
+ SecretId::parse(RHI_IDENTITY_KEY_SLOT)?,
+ BackendKind::External,
+ KeyVersion::new(1)?,
+ ))
+}
+
+fn secret_backend_failure(operation: Operation) -> radroots_secrets::Error {
+ radroots_secrets::Error::BackendFailure {
+ backend: BackendKind::External,
+ operation,
+ }
+}
+
+fn key_from_bytes(raw: &[u8]) -> Result<[u8; WRAPPING_KEY_BYTES], radroots_secrets::Error> {
+ raw.try_into()
+ .map_err(|_| secret_backend_failure(Operation::Read))
+}
+
+fn storage_error(path: &Path, operation: &str) -> IdentityError {
+ IdentityError::ProtectedStorage {
+ path: path.to_path_buf(),
+ message: operation.to_owned(),
+ }
+}
+
+pub fn encrypted_identity_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf {
+ let mut value = OsString::from(path.as_ref().as_os_str());
+ value.push(".key");
+ PathBuf::from(value)
+}
+
+pub fn store_encrypted_identity(
+ path: impl AsRef<Path>,
+ identity: &RadrootsIdentity,
+) -> Result<(), IdentityError> {
+ let path = path.as_ref();
+ if let Some(parent) = path.parent().filter(|value| !value.as_os_str().is_empty()) {
+ fs::create_dir_all(parent)
+ .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?;
+ }
let payload = serde_json::to_vec(&identity.to_file())?;
- radroots_runtime::seal_local_secret_file(path, RHI_IDENTITY_KEY_SLOT, &payload)?;
+ let plaintext = SecretMaterial::from_slice(payload.as_slice())
+ .map_err(|_| storage_error(path, "validate identity secret material"))?;
+ let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>())
+ .map_err(|_| storage_error(path, "validate identity data key"))?;
+ let wrapping = RhiFileKeyWrapping::new(path);
+ let envelope = futures_executor::block_on(EncryptedEnvelope::seal(
+ &wrapping,
+ SealRequest::new(
+ identity_secret_ref().map_err(|_| storage_error(path, "build identity reference"))?,
+ &plaintext,
+ SealMaterial::new(data_key, Nonce::new(rand::random())),
+ ),
+ ))
+ .map_err(|_| storage_error(path, "seal encrypted identity"))?;
+ let encoded = envelope
+ .encode()
+ .map_err(|_| storage_error(path, "encode encrypted identity"))?;
+ atomic_write(path, encoded.as_slice())
+}
+
+pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity, IdentityError> {
+ let path = path.as_ref();
+ let encoded = fs::read(path).map_err(|source| {
+ if source.kind() == std::io::ErrorKind::NotFound {
+ IdentityError::NotFound(path.to_path_buf())
+ } else {
+ IdentityError::Read(path.to_path_buf(), source)
+ }
+ })?;
+ let envelope = EncryptedEnvelope::decode(encoded.as_slice())
+ .map_err(|_| storage_error(path, "decode encrypted identity"))?;
+ let wrapping = RhiFileKeyWrapping::new(path);
+ let payload = futures_executor::block_on(envelope.open(&wrapping))
+ .map_err(|_| storage_error(path, "open encrypted identity"))?;
+ let file: RadrootsIdentityFile = payload
+ .expose_secret(|bytes| serde_json::from_slice(bytes))
+ .map_err(IdentityError::from)?;
+ RadrootsIdentity::try_from(file)
+}
+
+pub fn rotate_encrypted_identity(path: impl AsRef<Path>) -> Result<(), IdentityError> {
+ let path = path.as_ref();
+ let identity = load_encrypted_identity(path)?;
+ let key_path = encrypted_identity_wrapping_key_path(path);
+ let old_key =
+ fs::read(&key_path).map_err(|source| IdentityError::Read(key_path.clone(), source))?;
+ fs::remove_file(&key_path).map_err(|source| IdentityError::Write(key_path.clone(), source))?;
+ if let Err(error) = store_encrypted_identity(path, &identity) {
+ fs::write(&key_path, old_key)
+ .map_err(|source| IdentityError::Write(key_path.clone(), source))?;
+ set_secret_permissions(&key_path)
+ .map_err(|source| IdentityError::Write(key_path, source))?;
+ return Err(error);
+ }
Ok(())
}
-pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity> {
- let payload = radroots_runtime::open_local_secret_file(path, RHI_IDENTITY_KEY_SLOT)?;
- let file: RadrootsIdentityFile = serde_json::from_slice(&payload)?;
- Ok(RadrootsIdentity::try_from(file)?)
+pub fn load_identity_profile(
+ path: impl AsRef<Path>,
+) -> Result<RadrootsIdentityPublic, IdentityError> {
+ let path = path.as_ref();
+ let encoded = fs::read(path).map_err(|source| {
+ if source.kind() == std::io::ErrorKind::NotFound {
+ IdentityError::NotFound(path.to_path_buf())
+ } else {
+ IdentityError::Read(path.to_path_buf(), source)
+ }
+ })?;
+ serde_json::from_slice(encoded.as_slice()).map_err(IdentityError::from)
}
-fn resolved_identity_path(path: Option<&Path>) -> PathBuf {
- path.map(Path::to_path_buf).unwrap_or_else(|| {
- crate::paths::default_identity_path_for_process()
- .expect("resolve canonical rhi identity path")
- })
+pub fn store_identity_profile(
+ path: impl AsRef<Path>,
+ identity: &RadrootsIdentity,
+) -> Result<(), IdentityError> {
+ let encoded = serde_json::to_vec_pretty(&identity.to_public())?;
+ atomic_write(path.as_ref(), encoded.as_slice())
+}
+
+fn atomic_write(path: &Path, encoded: &[u8]) -> Result<(), IdentityError> {
+ let parent = path
+ .parent()
+ .filter(|value| !value.as_os_str().is_empty())
+ .unwrap_or_else(|| Path::new("."));
+ fs::create_dir_all(parent)
+ .map_err(|source| IdentityError::CreateDir(parent.to_path_buf(), source))?;
+ let mut temporary = tempfile::NamedTempFile::new_in(parent)
+ .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?;
+ temporary
+ .write_all(encoded)
+ .and_then(|()| temporary.as_file().sync_all())
+ .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?;
+ set_file_permissions(temporary.as_file())
+ .map_err(|source| IdentityError::Write(path.to_path_buf(), source))?;
+ temporary
+ .persist(path)
+ .map_err(|error| IdentityError::Write(path.to_path_buf(), error.error))?;
+ fs::File::open(parent)
+ .and_then(|directory| directory.sync_all())
+ .map_err(|source| IdentityError::Write(path.to_path_buf(), source))
+}
+
+#[cfg(unix)]
+fn set_secret_permissions(path: &Path) -> std::io::Result<()> {
+ use std::os::unix::fs::PermissionsExt;
+ fs::set_permissions(path, fs::Permissions::from_mode(0o600))
+}
+
+#[cfg(not(unix))]
+fn set_secret_permissions(_path: &Path) -> std::io::Result<()> {
+ Ok(())
+}
+
+fn set_file_permissions(file: &fs::File) -> std::io::Result<()> {
+ #[cfg(unix)]
+ {
+ use std::os::unix::fs::PermissionsExt;
+ file.set_permissions(fs::Permissions::from_mode(0o600))
+ }
+ #[cfg(not(unix))]
+ {
+ let _ = file;
+ Ok(())
+ }
}
#[cfg(test)]
mod tests {
- use super::{encrypted_identity_key_path, load_service_identity};
+ use super::*;
+
+ fn identity() -> RadrootsIdentity {
+ RadrootsIdentity::from_secret_key_str(
+ "1111111111111111111111111111111111111111111111111111111111111111",
+ )
+ .expect("identity")
+ }
#[test]
- fn load_service_identity_generates_encrypted_identity_artifacts() {
+ fn encrypted_identity_round_trips_and_rotates_wrapping_key() {
let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("rhi-identity.secret.json");
-
- let generated =
- load_service_identity(Some(&path), true).expect("generate encrypted identity");
- let loaded = load_service_identity(Some(&path), false).expect("load encrypted identity");
-
- assert_eq!(generated.id(), loaded.id());
- assert!(path.is_file());
- assert!(encrypted_identity_key_path(&path).is_file());
+ let path = temp.path().join("identity.enc");
+ let identity = identity();
+ store_encrypted_identity(&path, &identity).expect("store");
+ let key_path = encrypted_identity_wrapping_key_path(&path);
+ let before = fs::read(&key_path).expect("key before");
+ assert_eq!(
+ load_encrypted_identity(&path).expect("load").id(),
+ identity.id()
+ );
+ rotate_encrypted_identity(&path).expect("rotate");
+ assert_ne!(before, fs::read(key_path).expect("key after"));
+ assert_eq!(
+ load_encrypted_identity(&path).expect("load").id(),
+ identity.id()
+ );
}
#[test]
- fn load_service_identity_fails_when_wrapping_key_is_missing() {
+ fn public_profile_round_trips() {
let temp = tempfile::tempdir().expect("tempdir");
- let path = temp.path().join("rhi-identity.secret.json");
- let _ = load_service_identity(Some(&path), true).expect("generate encrypted identity");
- std::fs::remove_file(encrypted_identity_key_path(&path)).expect("remove wrapping key");
-
- let err = load_service_identity(Some(&path), false)
- .expect_err("missing wrapping key should fail");
- assert!(err.to_string().contains("identity"));
+ let path = temp.path().join("identity.json");
+ let identity = identity();
+ store_identity_profile(&path, &identity).expect("store profile");
+ assert_eq!(
+ load_identity_profile(path).expect("load profile").id,
+ identity.id()
+ );
}
}
diff --git a/src/lib.rs b/src/lib.rs
@@ -4,6 +4,10 @@ pub mod adapters;
pub mod cli;
pub mod config;
pub mod features;
+pub mod host_identity;
+pub mod host_nostr;
+pub mod host_paths;
+pub mod host_runtime;
pub mod identity_storage;
pub mod paths;
pub mod rhi;
@@ -12,8 +16,8 @@ pub use cli::Args as cli_args;
use anyhow::{Context, Result, anyhow, bail};
use radroots_event::{
- kinds::TRADE_MUTATION_EVENT_KINDS,
- profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier},
+ envelope::kind::TRADE_MUTATION_EVENT_KINDS,
+ profile::{AuthoredProfile, Nip05Identifier},
};
use std::time::Duration;
@@ -21,12 +25,10 @@ use crate::features::trade_agreement_attestation::{
TradeAgreementAttestationRuntime, TradeAgreementAttestationRuntimeConfig,
trade_mutation_subscription_kinds,
};
+use crate::host_nostr::{ApplicationHandlerSpec, Metadata, ProfileBuilder};
use crate::identity_storage::load_service_identity;
use crate::rhi::{Rhi, start_subscriber_with_policy};
-use radroots_nostr::prelude::{
- RadrootsNostrApplicationHandlerSpec, RadrootsNostrMetadata, RadrootsNostrProfileEventBuilder,
- radroots_nostr_build_profile_event, radroots_nostr_publish_application_handler,
-};
+use radroots_nostr::event::{build_application_handler, build_profile};
use tracing::{info, warn};
#[cfg(test)]
@@ -74,9 +76,9 @@ fn take_bootstrap_hook_result() -> Option<Result<(), String>> {
}
async fn bootstrap_presence(
- client: &radroots_nostr::prelude::RadrootsNostrClient,
- metadata: &RadrootsNostrMetadata,
- handler_spec: &RadrootsNostrApplicationHandlerSpec,
+ client: &crate::host_nostr::Client,
+ metadata: &Metadata,
+ handler_spec: &ApplicationHandlerSpec,
) -> Result<()> {
if let Some(result) = take_bootstrap_hook_result() {
return result.map_err(anyhow::Error::msg);
@@ -85,26 +87,31 @@ async fn bootstrap_presence(
client.connect().await;
client.wait_for_connection(Duration::from_secs(5)).await;
- let builder = build_authored_service_profile_event(metadata)?;
+ let profile_event = build_authored_service_profile_event(metadata)?
+ .sign_with_keys(client.keys())
+ .context("sign strict RHI service Profile")?;
client
- .send_profile_event_builder(builder)
+ .send_event(&profile_event)
.await
.context("publish strict RHI service Profile")?;
- radroots_nostr_publish_application_handler(client, handler_spec)
+ let handler_event = build_application_handler(handler_spec)
+ .context("build RHI application-handler event")?
+ .sign_with_keys(client.keys())
+ .context("sign RHI application-handler event")?;
+ client
+ .send_event(&handler_event)
.await
.context("publish RHI application-handler event")?;
Ok(())
}
-fn build_authored_service_profile_event(
- metadata: &RadrootsNostrMetadata,
-) -> Result<RadrootsNostrProfileEventBuilder> {
+fn build_authored_service_profile_event(metadata: &Metadata) -> Result<ProfileBuilder> {
let profile = authored_service_profile(metadata)?;
- radroots_nostr_build_profile_event(&profile).context("build strict RHI service Profile event")
+ build_profile(&profile).context("build strict RHI service Profile event")
}
-fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<RadrootsAuthoredProfile> {
+fn authored_service_profile(metadata: &Metadata) -> Result<AuthoredProfile> {
if metadata.picture.is_some() || metadata.banner.is_some() {
bail!(
"RHI service Profile media requires byte-verified Blossom descriptors and proven BUD-02 upload completion"
@@ -119,7 +126,7 @@ fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<Radroots
.clone()
.ok_or_else(|| anyhow!("RHI service Profile requires metadata.name"))?;
let mut profile =
- RadrootsAuthoredProfile::new(name).context("validate strict RHI service Profile name")?;
+ AuthoredProfile::new(name).context("validate strict RHI service Profile name")?;
if let Some(display_name) = metadata.display_name.as_ref() {
profile = profile.with_display_name(display_name.clone());
}
@@ -128,7 +135,7 @@ fn authored_service_profile(metadata: &RadrootsNostrMetadata) -> Result<Radroots
}
if let Some(nip05) = metadata.nip05.as_deref() {
profile = profile.with_nip05(
- RadrootsNip05Identifier::parse(nip05)
+ Nip05Identifier::parse(nip05)
.context("validate strict RHI service Profile NIP-05 identifier")?,
);
}
@@ -160,7 +167,7 @@ async fn wait_for_shutdown_or_stopped(handle: crate::rhi::RhiHandle) -> RunRhiWa
}
tokio::select! {
- _ = radroots_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown,
+ _ = crate::host_runtime::shutdown_signal() => RunRhiWaitOutcome::Shutdown,
_ = handle.stopped() => RunRhiWaitOutcome::Stopped,
}
}
@@ -196,14 +203,13 @@ pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()>
if !relays.is_empty() {
let handler_kinds = trade_mutation_subscription_kinds();
- let handler_spec = RadrootsNostrApplicationHandlerSpec {
- kinds: handler_kinds,
- identifier: service_cfg.nip89_identifier.clone(),
- metadata: Some(md.clone()),
- extra_tags: service_cfg.nip89_extra_tags.clone(),
- relays: relays.clone(),
- nostrconnect_url: None,
- };
+ let mut handler_spec = ApplicationHandlerSpec::new(handler_kinds)
+ .with_metadata(md.clone())
+ .with_extra_tags(service_cfg.nip89_extra_tags.clone())
+ .with_relays(relays.clone());
+ if let Some(identifier) = service_cfg.nip89_identifier.clone() {
+ handler_spec = handler_spec.with_identifier(identifier);
+ }
if let Err(e) = bootstrap_presence(&client, &md, &handler_spec).await {
warn!("Failed to publish service presence on startup: {e}");
} else {
@@ -260,17 +266,15 @@ mod tests {
run_rhi, run_rhi_bootstrap_hook, run_rhi_wait_hook,
};
use crate::{cli_args, config};
- use radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS;
+ use radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
- use std::sync::{Mutex, MutexGuard};
+ use tokio::sync::{Mutex, MutexGuard};
- static TEST_LOCK: Mutex<()> = Mutex::new(());
+ static TEST_LOCK: Mutex<()> = Mutex::const_new(());
- fn test_guard() -> MutexGuard<'static, ()> {
- let guard = TEST_LOCK
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner);
+ async fn test_guard() -> MutexGuard<'static, ()> {
+ let guard = TEST_LOCK.lock().await;
RUN_RHI_AUTO_STOP.store(false, Ordering::Relaxed);
RUN_RHI_SKIP_SUBSCRIBER.store(false, Ordering::Relaxed);
*run_rhi_bootstrap_hook()
@@ -286,7 +290,7 @@ mod tests {
config::Settings {
metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"),
config: config::Configuration {
- service: radroots_runtime::RadrootsNostrServiceConfig {
+ service: crate::host_runtime::NostrServiceConfig {
logs_dir: std::env::temp_dir()
.join("rhi-test-logs")
.display()
@@ -301,7 +305,7 @@ mod tests {
stdout: true,
},
subscriber: config::SubscriberConfig {
- backoff: radroots_runtime::BackoffConfig {
+ backoff: crate::host_runtime::BackoffConfig {
base_ms: 1,
max_ms: 2,
factor: 1,
@@ -321,7 +325,7 @@ mod tests {
fn args_for_identity(path: PathBuf) -> cli_args {
cli_args {
command: None,
- service: radroots_runtime::RadrootsServiceCliArgs {
+ service: crate::host_runtime::ServiceCliArgs {
config: Some(PathBuf::from("config.toml")),
identity: Some(path),
allow_generate_identity: true,
@@ -349,7 +353,7 @@ mod tests {
#[tokio::test]
async fn run_rhi_starts_and_stops_without_relays() {
- let _guard = test_guard();
+ let _guard = test_guard().await;
RUN_RHI_AUTO_STOP.store(true, Ordering::Relaxed);
let identity_path = unique_identity_path("no-relays");
let args = args_for_identity(identity_path);
@@ -359,7 +363,7 @@ mod tests {
#[tokio::test]
async fn run_rhi_bootstraps_release_product_handler_kinds_when_relays_exist() {
- let _guard = test_guard();
+ let _guard = test_guard().await;
RUN_RHI_SKIP_SUBSCRIBER.store(true, Ordering::Relaxed);
*run_rhi_bootstrap_hook()
.lock()
@@ -373,7 +377,7 @@ mod tests {
#[tokio::test]
async fn run_rhi_stops_on_wait_hook() {
- let _guard = test_guard();
+ let _guard = test_guard().await;
*run_rhi_wait_hook()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RunRhiWaitOutcome::Stopped);
@@ -385,23 +389,19 @@ mod tests {
#[tokio::test]
async fn bootstrap_presence_reports_hook_error() {
- let _guard = test_guard();
+ let _guard = test_guard().await;
*run_rhi_bootstrap_hook()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(Err("forced bootstrap failure".to_string()));
- let keys = radroots_nostr::prelude::RadrootsNostrKeys::generate();
- let client = radroots_nostr::prelude::RadrootsNostrClient::new(keys.clone());
- let metadata: radroots_nostr::prelude::RadrootsNostrMetadata =
+ let keys = crate::host_nostr::Keys::generate();
+ let client = crate::host_nostr::Client::new(keys.clone());
+ let metadata: crate::host_nostr::Metadata =
serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata");
- let spec = radroots_nostr::prelude::RadrootsNostrApplicationHandlerSpec {
- kinds: TRADE_MUTATION_EVENT_KINDS.to_vec(),
- identifier: Some("rhi".to_string()),
- metadata: Some(metadata.clone()),
- extra_tags: Vec::new(),
- relays: Vec::new(),
- nostrconnect_url: None,
- };
+ let spec =
+ crate::host_nostr::ApplicationHandlerSpec::new(TRADE_MUTATION_EVENT_KINDS.to_vec())
+ .with_identifier("rhi")
+ .with_metadata(metadata.clone());
let err = bootstrap_presence(&client, &metadata, &spec)
.await
.expect_err("forced error");
@@ -410,7 +410,7 @@ mod tests {
#[test]
fn service_profile_uses_only_strict_authored_fields() {
- let metadata: radroots_nostr::prelude::RadrootsNostrMetadata = serde_json::from_str(
+ let metadata: crate::host_nostr::Metadata = serde_json::from_str(
r#"{
"name":"rhi",
"display_name":"Radroots agreement attestation",
@@ -437,7 +437,7 @@ mod tests {
assert!(profile.picture().is_none());
assert!(profile.banner().is_none());
- let keys = radroots_nostr::prelude::RadrootsNostrKeys::generate();
+ let keys = crate::host_nostr::Keys::generate();
let event = build_authored_service_profile_event(&metadata)
.expect("strict Profile event")
.sign_with_keys(&keys)
diff --git a/src/main.rs b/src/main.rs
@@ -6,8 +6,6 @@ use anyhow::Result;
#[cfg(not(test))]
use clap::Parser;
#[cfg(not(test))]
-use radroots_log::{LogFileLayout, LoggingOptions};
-#[cfg(not(test))]
use rhi::cli::Command;
#[cfg(not(test))]
use rhi::features::trade_agreement_attestation::run_smoke_cli_command;
@@ -39,12 +37,14 @@ fn exit_code_from_run(result: Result<()>) -> ExitCode {
}
#[cfg(test)]
-static RUN_LOAD_HOOK: std::sync::OnceLock<
- std::sync::Mutex<Option<Result<(cli_args, config::Settings)>>>,
-> = std::sync::OnceLock::new();
+type RunLoadHookValue = Option<Result<(cli_args, config::Settings)>>;
+#[cfg(test)]
+type RunLoadHook = std::sync::Mutex<RunLoadHookValue>;
+#[cfg(test)]
+static RUN_LOAD_HOOK: std::sync::OnceLock<RunLoadHook> = std::sync::OnceLock::new();
#[cfg(test)]
-fn run_load_hook() -> &'static std::sync::Mutex<Option<Result<(cli_args, config::Settings)>>> {
+fn run_load_hook() -> &'static RunLoadHook {
RUN_LOAD_HOOK.get_or_init(|| std::sync::Mutex::new(None))
}
@@ -78,12 +78,12 @@ fn load_args_and_settings() -> Result<(cli_args, config::Settings)> {
{
return result;
}
- return Err(anyhow::anyhow!("run loader hook not set"));
+ Err(anyhow::anyhow!("run loader hook not set"))
}
#[cfg(not(test))]
{
- let args = cli_args::try_parse().map_err(radroots_runtime::RuntimeCliError::from)?;
+ let args = cli_args::try_parse()?;
let config_path = args
.service
.config
@@ -99,15 +99,31 @@ fn load_args_and_settings() -> Result<(cli_args, config::Settings)> {
#[cfg(not(test))]
fn init_rhi_logging(settings: &config::Settings) -> Result<()> {
- radroots_log::init_logging(LoggingOptions {
- dir: Some(settings.config.logging.output_dir.clone()),
- file_name: "rhi.log".to_owned(),
- stdout: settings.config.logging.stdout,
- default_level: Some(settings.config.logging.filter.clone()),
- file_layout: LogFileLayout::PrefixedDate,
- ..LoggingOptions::default()
- })
- .context("initialize logging")
+ use tracing_subscriber::fmt::writer::MakeWriterExt as _;
+
+ std::fs::create_dir_all(&settings.config.logging.output_dir)
+ .context("create RHI log directory")?;
+ let appender = tracing_appender::rolling::daily(&settings.config.logging.output_dir, "rhi.log");
+ let (writer, guard) = tracing_appender::non_blocking(appender);
+ static LOG_GUARD: std::sync::OnceLock<tracing_appender::non_blocking::WorkerGuard> =
+ std::sync::OnceLock::new();
+ let filter = tracing_subscriber::EnvFilter::new(&settings.config.logging.filter);
+ if settings.config.logging.stdout {
+ tracing_subscriber::fmt()
+ .with_env_filter(filter)
+ .with_writer(writer.and(std::io::stdout))
+ .try_init()
+ .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?;
+ } else {
+ tracing_subscriber::fmt()
+ .with_env_filter(filter)
+ .with_writer(writer)
+ .try_init()
+ .map_err(|error| anyhow::anyhow!("initialize RHI logging: {error}"))?;
+ }
+ LOG_GUARD
+ .set(guard)
+ .map_err(|_| anyhow::anyhow!("RHI logging is already initialized"))
}
fn runtime_startup_report(
@@ -215,7 +231,7 @@ fn log_runtime_startup_report(report: &RhiRuntimeStartupReport) {
async fn run() -> Result<()> {
#[cfg(not(test))]
{
- let args = cli_args::try_parse().map_err(radroots_runtime::RuntimeCliError::from)?;
+ let args = cli_args::try_parse()?;
if let Some(command) = args.command {
return match command {
Command::AttestationSmoke { .. } => run_smoke_cli_command(command).await,
@@ -244,24 +260,23 @@ mod tests {
RhiRuntimeStartupReport, exit_code_from_run, main, run, run_load_hook, run_rhi,
runtime_startup_report,
};
- use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys};
use rhi::features::trade_agreement_attestation::TradeAgreementAttestationRuntime;
+ use rhi::host_nostr::{Client, Keys};
use rhi::{cli_args, config, paths};
use std::path::PathBuf;
use std::process::ExitCode;
- static RUN_HOOK_TEST_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> =
- std::sync::OnceLock::new();
+ static RUN_HOOK_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
- fn run_hook_test_lock() -> &'static std::sync::Mutex<()> {
- RUN_HOOK_TEST_LOCK.get_or_init(|| std::sync::Mutex::new(()))
+ async fn run_hook_test_guard() -> tokio::sync::MutexGuard<'static, ()> {
+ RUN_HOOK_TEST_LOCK.lock().await
}
fn minimal_settings() -> config::Settings {
config::Settings {
metadata: serde_json::from_str(r#"{"name":"rhi-test"}"#).expect("metadata"),
config: config::Configuration {
- service: radroots_runtime::RadrootsNostrServiceConfig {
+ service: rhi::host_runtime::NostrServiceConfig {
logs_dir: std::env::temp_dir()
.join("rhi-test-logs")
.display()
@@ -329,7 +344,7 @@ mod tests {
async fn run_rhi_returns_error_when_identity_is_missing() {
let args = cli_args {
command: None,
- service: radroots_runtime::RadrootsServiceCliArgs {
+ service: rhi::host_runtime::ServiceCliArgs {
config: Some(PathBuf::from("config.toml")),
identity: Some(PathBuf::from("/tmp/rhi-missing-identity.secret.json")),
allow_generate_identity: false,
@@ -350,12 +365,10 @@ mod tests {
#[tokio::test]
async fn run_uses_injected_config_loader_result() {
- let _guard = run_hook_test_lock()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _guard = run_hook_test_guard().await;
let args = cli_args {
command: None,
- service: radroots_runtime::RadrootsServiceCliArgs {
+ service: rhi::host_runtime::ServiceCliArgs {
config: Some(PathBuf::from("config.toml")),
identity: Some(PathBuf::from("/tmp/rhi-run-hook-missing.secret.json")),
allow_generate_identity: false,
@@ -372,9 +385,7 @@ mod tests {
#[tokio::test]
async fn run_returns_error_when_loader_hook_is_absent() {
- let _guard = run_hook_test_lock()
- .lock()
- .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let _guard = run_hook_test_guard().await;
*run_load_hook()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
@@ -387,13 +398,13 @@ mod tests {
#[tokio::test]
async fn non_test_start_subscriber_path_can_start_and_stop() {
- let keys = RadrootsNostrKeys::generate();
- let client = RadrootsNostrClient::new(keys.clone());
+ let keys = Keys::generate();
+ let client = Client::new(keys.clone());
let handle = rhi::rhi::start_subscriber(
client,
keys,
TradeAgreementAttestationRuntime::new(),
- radroots_runtime::BackoffConfig {
+ rhi::host_runtime::BackoffConfig {
base_ms: 1,
max_ms: 2,
factor: 1,
@@ -409,7 +420,7 @@ mod tests {
#[test]
fn runtime_startup_report_prefers_explicit_cli_paths() {
let args = cli_args {
- service: radroots_runtime::RadrootsServiceCliArgs {
+ service: rhi::host_runtime::ServiceCliArgs {
config: Some(PathBuf::from("/tmp/rhi/config.toml")),
identity: Some(PathBuf::from("/tmp/rhi/identity.secret.json")),
allow_generate_identity: false,
@@ -457,7 +468,7 @@ mod tests {
fn runtime_startup_report_falls_back_to_canonical_contract_paths() {
let args = cli_args {
command: None,
- service: radroots_runtime::RadrootsServiceCliArgs {
+ service: rhi::host_runtime::ServiceCliArgs {
config: None,
identity: None,
allow_generate_identity: false,
diff --git a/src/paths.rs b/src/paths.rs
@@ -1,13 +1,14 @@
use std::path::{Path, PathBuf};
-use anyhow::{Context, Result, bail};
-use radroots_runtime_paths::{
- DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME, RadrootsPathOverrides,
- RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace,
+use crate::host_paths::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace,
};
+use anyhow::{Context, Result, bail};
use serde::Serialize;
const RHI_RUNTIME_ID: &str = "rhi";
+const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml";
+const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json";
const SUBSCRIBER_STATE_DIR_NAME: &str = "trade-agreement-attestation";
const SUBSCRIBER_STATE_FILE_NAME: &str = "state.json";
const RHI_PATHS_PROFILE_ENV: &str = "RHI_PATHS_PROFILE";
diff --git a/src/rhi.rs b/src/rhi.rs
@@ -3,8 +3,8 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
-use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys};
-use radroots_runtime::{Backoff, BackoffConfig};
+use crate::host_nostr::{Client, Keys};
+use crate::host_runtime::{Backoff, BackoffConfig};
use tokio::sync::Mutex;
use crate::features::trade_agreement_attestation::{
@@ -33,8 +33,8 @@ fn subscriber_result_hook()
}
async fn run_subscriber_once(
- client: RadrootsNostrClient,
- keys: RadrootsNostrKeys,
+ client: Client,
+ keys: Keys,
runtime: TradeAgreementAttestationRuntime,
policy: TradeAgreementAttestationPolicy,
stop_rx: tokio::sync::watch::Receiver<bool>,
@@ -53,7 +53,7 @@ async fn run_subscriber_once(
}
async fn wait_for_connection_or_stop(
- client: &RadrootsNostrClient,
+ client: &Client,
stop_rx: &mut tokio::sync::watch::Receiver<bool>,
) -> bool {
if *stop_rx.borrow() {
@@ -67,18 +67,18 @@ async fn wait_for_connection_or_stop(
pub struct Rhi {
pub(crate) _started: Instant,
- pub client: RadrootsNostrClient,
+ pub client: Client,
pub(crate) agreement_attestation_runtime: TradeAgreementAttestationRuntime,
pub(crate) agreement_attestation_policy: TradeAgreementAttestationPolicy,
}
impl Rhi {
- pub fn new(keys: RadrootsNostrKeys) -> Self {
+ pub fn new(keys: Keys) -> Self {
Self::with_agreement_attestation_runtime(keys, TradeAgreementAttestationRuntime::new())
}
pub fn with_agreement_attestation_runtime(
- keys: RadrootsNostrKeys,
+ keys: Keys,
agreement_attestation_runtime: TradeAgreementAttestationRuntime,
) -> Self {
Self::with_agreement_attestation_runtime_and_policy(
@@ -89,11 +89,11 @@ impl Rhi {
}
pub fn with_agreement_attestation_runtime_and_policy(
- keys: RadrootsNostrKeys,
+ keys: Keys,
agreement_attestation_runtime: TradeAgreementAttestationRuntime,
agreement_attestation_policy: TradeAgreementAttestationPolicy,
) -> Self {
- let client = RadrootsNostrClient::new(keys);
+ let client = Client::new(keys);
Self {
_started: Instant::now(),
client,
@@ -132,8 +132,8 @@ impl RhiHandle {
}
pub async fn start_subscriber(
- client: RadrootsNostrClient,
- keys: RadrootsNostrKeys,
+ client: Client,
+ keys: Keys,
runtime: TradeAgreementAttestationRuntime,
backoff_cfg: BackoffConfig,
) -> RhiHandle {
@@ -148,8 +148,8 @@ pub async fn start_subscriber(
}
pub async fn start_subscriber_with_policy(
- client: RadrootsNostrClient,
- keys: RadrootsNostrKeys,
+ client: Client,
+ keys: Keys,
runtime: TradeAgreementAttestationRuntime,
policy: TradeAgreementAttestationPolicy,
backoff_cfg: BackoffConfig,
@@ -212,15 +212,15 @@ mod tests {
Rhi, RhiHandle, start_subscriber, subscriber_result_hook, wait_for_connection_or_stop,
};
use crate::features::trade_agreement_attestation::TradeAgreementAttestationRuntime;
+ use crate::host_nostr::{Client, Keys};
+ use crate::host_runtime::BackoffConfig;
use anyhow::anyhow;
- use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys};
- use radroots_runtime::BackoffConfig;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn rhi_new_initializes_client_and_runtime() {
- let keys = RadrootsNostrKeys::generate();
+ let keys = Keys::generate();
let rhi = Rhi::new(keys);
let _ = rhi.client.clone();
assert!(rhi.agreement_attestation_runtime.reports().await.is_empty());
@@ -242,7 +242,7 @@ mod tests {
#[tokio::test]
async fn start_subscriber_runs_with_and_without_relay() {
- let keys = RadrootsNostrKeys::generate();
+ let keys = Keys::generate();
let cfg = BackoffConfig {
base_ms: 1,
max_ms: 2,
@@ -250,7 +250,7 @@ mod tests {
jitter_ms: 0,
};
- let client_err = RadrootsNostrClient::new(keys.clone());
+ let client_err = Client::new(keys.clone());
let handle_err = start_subscriber(
client_err,
keys.clone(),
@@ -262,7 +262,7 @@ mod tests {
handle_err.stop();
handle_err.stopped().await;
- let client_ok = RadrootsNostrClient::new(keys.clone());
+ let client_ok = Client::new(keys.clone());
let _ = client_ok.add_relay("wss://relay.example.com").await;
subscriber_result_hook()
.lock()
@@ -282,8 +282,8 @@ mod tests {
#[tokio::test]
async fn start_subscriber_stops_during_connection_wait_branch() {
- let keys = RadrootsNostrKeys::generate();
- let client = RadrootsNostrClient::new(keys.clone());
+ let keys = Keys::generate();
+ let client = Client::new(keys.clone());
let handle = start_subscriber(
client,
keys,
@@ -303,8 +303,8 @@ mod tests {
#[tokio::test]
async fn start_subscriber_stops_during_backoff_wait_branch() {
- let keys = RadrootsNostrKeys::generate();
- let client = RadrootsNostrClient::new(keys.clone());
+ let keys = Keys::generate();
+ let client = Client::new(keys.clone());
let _ = client.add_relay("wss://relay.example.com").await;
subscriber_result_hook()
.lock()
@@ -329,15 +329,15 @@ mod tests {
#[tokio::test]
async fn wait_for_connection_or_stop_covers_both_outcomes() {
- let keys = RadrootsNostrKeys::generate();
+ let keys = Keys::generate();
- let client_stop = RadrootsNostrClient::new(keys.clone());
+ let client_stop = Client::new(keys.clone());
let (stop_tx, mut stop_rx) = tokio::sync::watch::channel(false);
let _ = stop_tx.send(true);
let stop_branch = wait_for_connection_or_stop(&client_stop, &mut stop_rx).await;
assert!(!stop_branch);
- let client_wait = RadrootsNostrClient::new(keys);
+ let client_wait = Client::new(keys);
let (_tx, mut rx) = tokio::sync::watch::channel(false);
let wait_branch = wait_for_connection_or_stop(&client_wait, &mut rx).await;
assert!(wait_branch);
diff --git a/tests/source_guards.rs b/tests/source_guards.rs
@@ -34,9 +34,12 @@ fn rhi_manifest_exact_pins_radroots_contract() {
if !name.starts_with("radroots_") {
continue;
}
+ let version = dependency
+ .as_str()
+ .or_else(|| dependency.get("version").and_then(toml::Value::as_str));
assert_eq!(
- dependency["version"].as_str(),
- Some("=1.0.0-alpha.1"),
+ version,
+ Some("=0.1.0-alpha"),
"RHI must exact-pin {name} to the governed event contract release"
);
}
@@ -118,15 +121,13 @@ fn rhi_agreement_attestation_is_release_product_optional_infrastructure() {
assert!(
lib.contains("trade_mutation_subscription_kinds()")
- && lib.contains("&TRADE_MUTATION_EVENT_KINDS")
- && lib.contains("RadrootsAuthoredProfile")
- && lib.contains("RadrootsNostrProfileEventBuilder")
- && lib.contains("radroots_nostr_build_profile_event")
- && lib.contains("send_profile_event_builder")
- && !lib.contains("radroots_nostr_build_event")
- && lib.contains("radroots_nostr_publish_application_handler")
- && !lib.contains("radroots_nostr_bootstrap_service_presence")
- && !lib.contains("RadrootsProfileType")
+ && lib.contains("TRADE_MUTATION_EVENT_KINDS")
+ && lib.contains("AuthoredProfile")
+ && lib.contains("ProfileBuilder")
+ && lib.contains("build_profile")
+ && lib.contains("build_application_handler")
+ && lib.contains(".send_event(")
+ && !lib.contains("radroots_nostr::prelude")
&& lib.contains("client.into_inner()"),
"RHI service presence must advertise canonical release-product trade mutation kinds"
);