radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 7ef25367c4516f0312c769adb310f5fe8bab64d9
parent a854579e00d43d8741f3afeba80946811ac06c51
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 18:26:28 +0000

radrootsd: validate packaged sdk protocol integration

- depend on exact final registry crate identities without sibling source paths
- keep daemon lifecycle paths logging and secret persistence host-owned
- validate V5 publish requests authorization errors and response invariants
- prove daemon and SDK adapter gates against extracted release artifacts

Diffstat:
MCargo.lock | 483++++++-------------------------------------------------------------------------
MCargo.toml | 26++++++++++++++------------
Msrc/app/cli.rs | 29++++++++++++++++++++++++++---
Msrc/app/config.rs | 77++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/app/identity_storage.rs | 220++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/app/paths.rs | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/app/runtime.rs | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mtests/source_boundary.rs | 117+++++++++++++++++++++++++++++++++-----------------------------------------------
8 files changed, 715 insertions(+), 655 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" @@ -51,15 +39,6 @@ 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,12 +95,6 @@ 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" @@ -204,12 +177,6 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" @@ -259,9 +226,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" @@ -343,19 +307,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" @@ -422,66 +373,12 @@ dependencies = [ ] [[package]] -name = "config" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust2", -] - -[[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -530,12 +427,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] name = "crypto-bigint" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -606,15 +497,6 @@ 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 = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -647,15 +529,6 @@ dependencies = [ ] [[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -933,16 +806,6 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" @@ -963,15 +826,6 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" @@ -1092,30 +946,6 @@ dependencies = [ ] [[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" @@ -1296,17 +1126,6 @@ dependencies = [ ] [[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] name = "jsonrpsee" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1476,12 +1295,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" @@ -1499,23 +1312,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", @@ -1630,16 +1433,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" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1680,12 +1473,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" @@ -1702,49 +1489,6 @@ 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" version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1917,18 +1661,6 @@ dependencies = [ ] [[package]] -name = "radroots_log" -version = "0.1.0-alpha" -dependencies = [ - "chrono", - "serde_json", - "thiserror 1.0.69", - "tracing", - "tracing-appender", - "tracing-subscriber", -] - -[[package]] name = "radroots_nostr" version = "0.1.0-alpha" dependencies = [ @@ -1942,18 +1674,6 @@ dependencies = [ ] [[package]] -name = "radroots_protected_store" -version = "0.1.0-alpha" -dependencies = [ - "chacha20poly1305", - "getrandom 0.2.17", - "radroots_secret_vault", - "serde", - "serde_json", - "zeroize", -] - -[[package]] name = "radroots_protocol" version = "0.1.0-alpha" dependencies = [ @@ -1961,41 +1681,14 @@ dependencies = [ ] [[package]] -name = "radroots_runtime" +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 = "0.1.0-alpha" -dependencies = [ - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "radroots_secret_vault" -version = "0.1.0-alpha" - -[[package]] name = "radroots_transport" version = "0.1.0-alpha" dependencies = [ @@ -2027,6 +1720,7 @@ name = "radrootsd" version = "0.1.0" dependencies = [ "anyhow", + "chacha20poly1305", "clap", "futures-executor", "jsonrpsee", @@ -2037,8 +1731,7 @@ dependencies = [ "radroots_identity", "radroots_nostr", "radroots_protocol", - "radroots_runtime", - "radroots_runtime_paths", + "radroots_secrets", "radroots_transport", "radroots_transport_nostr", "rand 0.9.2", @@ -2050,10 +1743,14 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "toml", "tower", "tracing", + "tracing-appender", + "tracing-subscriber", "url", "uuid", + "zeroize", ] [[package]] @@ -2156,34 +1853,12 @@ 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 = "route-recognizer" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[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" @@ -2382,11 +2057,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2464,7 +2139,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures", "http", @@ -2500,7 +2175,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "cfg-if", "crc", @@ -2512,7 +2187,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink 0.11.1", + "hashlink", "indexmap", "log", "memchr", @@ -2724,15 +2399,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" @@ -2851,44 +2517,42 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow 0.7.15", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.27" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", + "winnow 1.0.4", ] [[package]] -name = "toml_write" -version = "0.1.2" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" @@ -3016,12 +2680,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3049,12 +2707,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" @@ -3294,65 +2946,12 @@ 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" @@ -3439,9 +3038,12 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -3538,17 +3140,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 0.8.4", -] - -[[package]] name = "yoke" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -11,15 +11,14 @@ description = "Radroots local runtime daemon for storage, sync, and relay publis resolver = "3" [workspace.dependencies] -radroots_event = { path = "../lib/crates/event", version = "=0.1.0-alpha" } -radroots_event_codec = { path = "../lib/crates/event_codec", version = "=0.1.0-alpha" } -radroots_identity = { path = "../lib/crates/identity", version = "=0.1.0-alpha" } -radroots_nostr = { path = "../lib/crates/nostr", version = "=0.1.0-alpha" } -radroots_protocol = { path = "../lib/crates/protocol", version = "=0.1.0-alpha", default-features = false } -radroots_transport_nostr = { path = "../lib/crates/transport_nostr", version = "=0.1.0-alpha", default-features = false } -radroots_transport = { path = "../lib/crates/transport", version = "=0.1.0-alpha", default-features = false } -radroots_runtime = { path = "../lib/crates/runtime", version = "=0.1.0-alpha" } -radroots_runtime_paths = { path = "../lib/crates/runtime_paths", version = "=0.1.0-alpha" } +radroots_event = { version = "=0.1.0-alpha" } +radroots_event_codec = { version = "=0.1.0-alpha" } +radroots_identity = { version = "=0.1.0-alpha" } +radroots_nostr = { version = "=0.1.0-alpha" } +radroots_protocol = { version = "=0.1.0-alpha", default-features = false } +radroots_secrets = { version = "=0.1.0-alpha", default-features = false } +radroots_transport_nostr = { version = "=0.1.0-alpha", default-features = false } +radroots_transport = { version = "=0.1.0-alpha", default-features = false } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } @@ -30,14 +29,14 @@ radroots_event_codec = { workspace = true, features = ["json"] } radroots_identity = { workspace = true } radroots_nostr = { workspace = true, features = ["events"] } radroots_protocol = { workspace = true, features = ["std", "serde"] } +radroots_secrets = { workspace = true, features = ["std"] } radroots_transport_nostr = { workspace = true } radroots_transport = { workspace = true } -radroots_runtime = { workspace = true, features = ["cli"] } -radroots_runtime_paths = { workspace = true } nostr = { version = "0.44.2", features = ["nip46"] } nostr-sdk = { version = "0.44.1" } anyhow = { version = "1" } +chacha20poly1305 = { version = "0.10" } clap = { version = "4", features = ["derive"] } jsonrpsee = { version = "0.26", features = ["server"] } futures-executor = { version = "0.3" } @@ -49,10 +48,13 @@ sha2 = { version = "0.10" } sqlx = { version = "0.9.0", default-features = false, features = ["derive", "sqlite-bundled"] } tokio = { version = "1", features = ["full"] } thiserror = { version = "2" } +toml = { version = "0.9" } tower = { version = "0.5.3", features = ["util"] } tracing = { version = "0.1" } +tracing-appender = { version = "0.2" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.22.0", features = ["v4"] } url = { version = "2.5.8" } +zeroize = { version = "1" } -[dev-dependencies] tempfile = { version = "3" } diff --git a/src/app/cli.rs b/src/app/cli.rs @@ -1,7 +1,30 @@ use std::path::PathBuf; -use clap::{Args as ClapArgs, Parser, Subcommand}; -use radroots_runtime::RadrootsServiceCliArgs; +use clap::{ArgAction, Args as ClapArgs, Parser, Subcommand, ValueHint}; + +#[derive(ClapArgs, Debug, Clone)] +pub struct ServiceCliArgs { + #[arg( + long, + value_name = "PATH", + value_hint = ValueHint::FilePath, + help = "Path to the daemon configuration file; no implicit cwd-rooted default is used" + )] + pub config: Option<PathBuf>, + #[arg( + long, + value_name = "PATH", + value_hint = ValueHint::FilePath, + help = "Path to the daemon encrypted identity envelope" + )] + pub identity: Option<PathBuf>, + #[arg( + long, + action = ArgAction::SetTrue, + help = "Allow generating a new encrypted identity when the configured path is missing" + )] + pub allow_generate_identity: bool, +} #[derive(Parser, Debug, Clone)] #[command( @@ -11,7 +34,7 @@ use radroots_runtime::RadrootsServiceCliArgs; )] pub struct Args { #[command(flatten)] - pub service: RadrootsServiceCliArgs, + pub service: ServiceCliArgs, #[command(subcommand)] pub command: Option<Command>, } diff --git a/src/app/config.rs b/src/app/config.rs @@ -1,15 +1,25 @@ use crate::host_nostr::Metadata; use anyhow::{Context, Result, bail}; use radroots_event::profile::{AuthoredProfile, Nip05Identifier}; -use radroots_runtime::RadrootsNostrServiceConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use super::paths::{ - RadrootsdRuntimePaths, default_transport_publish_database_path, process_path_selection, - resolve_runtime_paths_with_resolver, + PathProfile, PathResolver, RadrootsdRuntimePaths, default_transport_publish_database_path, + process_path_selection, resolve_runtime_paths_with_resolver, }; +#[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>>, +} + fn default_rpc_addr() -> String { "127.0.0.1:7070".to_string() } @@ -91,8 +101,8 @@ struct RawServiceConfig { } impl RawServiceConfig { - fn into_service_config(self, paths: &RadrootsdRuntimePaths) -> RadrootsNostrServiceConfig { - RadrootsNostrServiceConfig { + fn into_service_config(self, paths: &RadrootsdRuntimePaths) -> NostrServiceConfig { + NostrServiceConfig { logs_dir: self .logs_dir .unwrap_or_else(|| paths.logs_dir.display().to_string()), @@ -200,12 +210,14 @@ impl RawSettings { fn load_settings_from_path_with_resolver( path: &Path, - resolver: &radroots_runtime_paths::RadrootsPathResolver, - profile: radroots_runtime_paths::RadrootsPathProfile, + resolver: &PathResolver, + profile: PathProfile, repo_local_root: Option<&Path>, ) -> Result<Settings> { - let raw: RawSettings = radroots_runtime::load_required_file(path) - .with_context(|| format!("load configuration from {}", path.display()))?; + let source = std::fs::read_to_string(path) + .with_context(|| format!("read configuration from {}", path.display()))?; + let raw: RawSettings = toml::from_str(source.as_str()) + .with_context(|| format!("parse configuration from {}", path.display()))?; let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?; let settings = raw.into_settings(&paths); settings.validate()?; @@ -217,7 +229,7 @@ pub fn load_settings_from_path(path: impl AsRef<Path>) -> Result<Settings> { let (profile, repo_local_root) = process_path_selection()?; load_settings_from_path_with_resolver( path, - &radroots_runtime_paths::RadrootsPathResolver::current(), + &PathResolver::current(), profile, repo_local_root.as_deref(), ) @@ -366,7 +378,7 @@ impl Default for RpcConfig { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Configuration { #[serde(flatten)] - pub service: RadrootsNostrServiceConfig, + pub service: NostrServiceConfig, #[serde(default)] pub rpc: RpcConfig, #[serde(default)] @@ -477,40 +489,39 @@ impl Settings { mod tests { use std::path::PathBuf; + use super::NostrServiceConfig; use super::{ Configuration, Nip46Config, NostrRelayUrlPolicy, RpcConfig, TransportPublishConfig, load_settings_from_path_with_resolver, }; use crate::app::paths::{ + HostEnvironment, PathProfile, PathResolver, Platform, RuntimePathSelection, + }; + use crate::app::paths::{ RadrootsdRuntimeContractOutput, default_runtime_paths_for_process, resolve_runtime_paths_with_resolver, runtime_contract_with_selection, }; use radroots_event::profile::Nip05Identifier; - use radroots_runtime::RadrootsNostrServiceConfig; - use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, - RadrootsRuntimePathSelection, - }; use serde_json::json; - fn linux_resolver(home: &str) -> RadrootsPathResolver { - RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment { + fn linux_resolver(home: &str) -> PathResolver { + PathResolver::new( + Platform::Linux, + HostEnvironment { home_dir: Some(PathBuf::from(home)), - ..RadrootsHostEnvironment::default() + ..HostEnvironment::default() }, ) } - fn service_config() -> RadrootsNostrServiceConfig { + fn service_config() -> NostrServiceConfig { let paths = resolve_runtime_paths_with_resolver( &linux_resolver("/home/treesap"), - RadrootsPathProfile::InteractiveUser, + PathProfile::InteractiveUser, None, ) .expect("resolve interactive-user paths"); - RadrootsNostrServiceConfig { + NostrServiceConfig { logs_dir: paths.logs_dir.display().to_string(), relays: Vec::new(), nip89_identifier: Some("radrootsd".to_string()), @@ -519,13 +530,13 @@ mod tests { } fn runtime_contract_with_resolver( - resolver: &RadrootsPathResolver, - profile: RadrootsPathProfile, + resolver: &PathResolver, + profile: PathProfile, repo_local_root: Option<&std::path::Path>, ) -> anyhow::Result<RadrootsdRuntimeContractOutput> { runtime_contract_with_selection( resolver, - &RadrootsRuntimePathSelection::caller(profile, repo_local_root.map(PathBuf::from)), + &RuntimePathSelection::caller(profile, repo_local_root.map(PathBuf::from)), ) } @@ -695,7 +706,7 @@ mod tests { fn runtime_paths_follow_interactive_user_contract() { let paths = resolve_runtime_paths_with_resolver( &linux_resolver("/home/treesap"), - RadrootsPathProfile::InteractiveUser, + PathProfile::InteractiveUser, None, ) .expect("resolve interactive-user paths"); @@ -726,7 +737,7 @@ mod tests { fn runtime_paths_follow_service_host_contract() { let paths = resolve_runtime_paths_with_resolver( &linux_resolver("/home/treesap"), - RadrootsPathProfile::ServiceHost, + PathProfile::ServiceHost, None, ) .expect("resolve service-host paths"); @@ -754,7 +765,7 @@ mod tests { let repo_local_root = PathBuf::from("/repo/.local/radroots/dev/radrootsd"); let paths = resolve_runtime_paths_with_resolver( &linux_resolver("/home/treesap"), - RadrootsPathProfile::RepoLocal, + PathProfile::RepoLocal, Some(repo_local_root.as_path()), ) .expect("resolve repo-local paths"); @@ -799,7 +810,7 @@ addr = "127.0.0.1:7070" let settings = load_settings_from_path_with_resolver( &config_path, &linux_resolver("/home/treesap"), - RadrootsPathProfile::InteractiveUser, + PathProfile::InteractiveUser, None, ) .expect("load settings"); @@ -838,7 +849,7 @@ relay_url_policy = "localhost" let err = load_settings_from_path_with_resolver( &config_path, &linux_resolver("/home/treesap"), - RadrootsPathProfile::InteractiveUser, + PathProfile::InteractiveUser, None, ) .expect_err("obsolete transport_publish config should fail"); @@ -851,7 +862,7 @@ relay_url_policy = "localhost" fn runtime_contract_output_matches_interactive_user_contract() { let contract = runtime_contract_with_resolver( &linux_resolver("/home/treesap"), - RadrootsPathProfile::InteractiveUser, + PathProfile::InteractiveUser, None, ) .expect("interactive-user contract"); diff --git a/src/app/identity_storage.rs b/src/app/identity_storage.rs @@ -1,10 +1,153 @@ +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::io::Write; use std::path::{Path, PathBuf}; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; +use chacha20poly1305::aead::{Aead, KeyInit, Payload}; +use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; use nostr::{Keys, SecretKey}; +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 serde::{Deserialize, Serialize}; +use zeroize::Zeroize; const RADROOTSD_IDENTITY_KEY_SLOT: &str = "radrootsd_identity"; +const WRAPPING_KEY_BYTES: usize = 32; +const WRAPPING_NONCE_BYTES: usize = 24; +const WRAPPED_KEY_VERSION: u8 = 1; + +struct DaemonFileKeyWrapping { + key_path: PathBuf, +} + +impl DaemonFileKeyWrapping { + fn new(identity_path: &Path) -> Self { + Self { + key_path: encrypted_identity_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(|_| radroots_secrets::Error::BackendFailure { + backend: BackendKind::External, + operation: radroots_secrets::error::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 DaemonFileKeyWrapping { + 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(RADROOTSD_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)) +} /// Host-private service signing identity. /// @@ -89,9 +232,10 @@ impl DaemonIdentity { } } -#[cfg(test)] pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf { - radroots_runtime::local_wrapping_key_path(path) + let mut value = OsString::from(path.as_ref().as_os_str()); + value.push(".key"); + PathBuf::from(value) } pub fn load_service_identity(path: Option<&Path>, allow_generate: bool) -> Result<DaemonIdentity> { @@ -112,17 +256,81 @@ pub fn load_service_identity(path: Option<&Path>, allow_generate: bool) -> Resul } pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &DaemonIdentity) -> Result<()> { + let path = path.as_ref(); + if let Some(parent) = path.parent().filter(|path| !path.as_os_str().is_empty()) { + fs::create_dir_all(parent)?; + } let payload = serde_json::to_vec(&identity.to_file())?; - radroots_runtime::seal_local_secret_file(path, RADROOTSD_IDENTITY_KEY_SLOT, &payload)?; + let plaintext = SecretMaterial::from_slice(payload.as_slice())?; + let data_key = SecretMaterial::from_slice(&rand::random::<[u8; 32]>())?; + let nonce = Nonce::new(rand::random()); + let wrapping = DaemonFileKeyWrapping::new(path); + let envelope = futures_executor::block_on(EncryptedEnvelope::seal( + &wrapping, + SealRequest::new( + identity_secret_ref()?, + &plaintext, + SealMaterial::new(data_key, nonce), + ), + ))?; + let encoded = envelope.encode()?; + let mut temporary = tempfile::NamedTempFile::new_in( + path.parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or(Path::new(".")), + )?; + temporary.write_all(encoded.as_slice())?; + temporary.as_file().sync_all()?; + set_file_permissions(temporary.as_file())?; + temporary.persist(path)?; + sync_parent(path)?; Ok(()) } pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<DaemonIdentity> { - let payload = radroots_runtime::open_local_secret_file(path, RADROOTSD_IDENTITY_KEY_SLOT)?; - let file: DaemonIdentityFile = serde_json::from_slice(&payload)?; + let path = path.as_ref(); + let encoded = fs::read(path)?; + let envelope = EncryptedEnvelope::decode(encoded.as_slice()) + .context("decode encrypted daemon identity")?; + let wrapping = DaemonFileKeyWrapping::new(path); + let payload = futures_executor::block_on(envelope.open(&wrapping)) + .context("open encrypted daemon identity")?; + let file: DaemonIdentityFile = payload.expose_secret(|bytes| serde_json::from_slice(bytes))?; DaemonIdentity::from_file(file) } +#[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(()) + } +} + +fn sync_parent(path: &Path) -> std::io::Result<()> { + let parent = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + fs::File::open(parent)?.sync_all() +} + fn resolved_identity_path(path: Option<&Path>) -> PathBuf { path.map(Path::to_path_buf).unwrap_or_else(|| { crate::app::paths::default_identity_path_for_process() diff --git a/src/app/paths.rs b/src/app/paths.rs @@ -1,14 +1,11 @@ use std::path::{Path, PathBuf}; -use anyhow::Result; -use radroots_runtime_paths::{ - DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME, RadrootsPathProfile, - RadrootsPathResolver, RadrootsRuntimePathSelection, RadrootsRuntimeSelectionContract, - RadrootsRuntimeSelectionOverrideContract, -}; +use anyhow::{Result, bail}; use serde::Serialize; const RADROOTSD_RUNTIME_ID: &str = "radrootsd"; +const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml"; +const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json"; const TRANSPORT_PUBLISH_DATABASE_FILE_NAME: &str = "transport_publish.sqlite"; const RADROOTSD_PATHS_PROFILE_ENV: &str = "RADROOTSD_PATHS_PROFILE"; const RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RADROOTSD_PATHS_REPO_LOCAL_ROOT"; @@ -21,6 +18,229 @@ const SUBORDINATE_PATH_OVERRIDE_KEYS: [&str; 2] = [ "config.transport_publish.database_path", ]; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub(crate) enum Platform { + Linux, + Macos, + Windows, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct HostEnvironment { + pub(crate) home_dir: Option<PathBuf>, + pub(crate) appdata_dir: Option<PathBuf>, + pub(crate) localappdata_dir: Option<PathBuf>, + pub(crate) programdata_dir: Option<PathBuf>, +} + +impl HostEnvironment { + fn current() -> 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), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PathProfile { + InteractiveUser, + ServiceHost, + RepoLocal, +} + +impl PathProfile { + fn parse(value: &str) -> Result<Self> { + match value { + "interactive_user" => Ok(Self::InteractiveUser), + "service_host" => Ok(Self::ServiceHost), + "repo_local" => Ok(Self::RepoLocal), + _ => bail!("unknown radrootsd path profile `{value}`"), + } + } + + const fn as_str(self) -> &'static str { + match self { + Self::InteractiveUser => "interactive_user", + Self::ServiceHost => "service_host", + Self::RepoLocal => "repo_local", + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct PathResolver { + platform: Platform, + environment: HostEnvironment, +} + +impl PathResolver { + pub(crate) const fn new(platform: Platform, environment: HostEnvironment) -> Self { + Self { + platform, + environment, + } + } + + pub(crate) fn current() -> Self { + #[cfg(target_os = "windows")] + let platform = Platform::Windows; + #[cfg(target_os = "macos")] + let platform = Platform::Macos; + #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] + let platform = Platform::Linux; + Self::new(platform, HostEnvironment::current()) + } + + fn roots(&self, profile: PathProfile, repo_local_root: Option<&Path>) -> Result<RuntimeRoots> { + match profile { + PathProfile::RepoLocal => repo_local_root + .map(RuntimeRoots::from_base) + .ok_or_else(|| anyhow::anyhow!("repo_local requires an explicit root")), + PathProfile::ServiceHost => match self.platform { + Platform::Linux | Platform::Macos => Ok(RuntimeRoots { + config: PathBuf::from("/etc/radroots"), + data: PathBuf::from("/var/lib/radroots"), + logs: PathBuf::from("/var/log/radroots"), + secrets: PathBuf::from("/etc/radroots/secrets"), + }), + Platform::Windows => { + let base = self + .environment + .programdata_dir + .as_deref() + .ok_or_else(|| anyhow::anyhow!("PROGRAMDATA is required"))? + .join("Radroots"); + Ok(RuntimeRoots { + config: base.join("config"), + data: base.join("data"), + logs: base.join("logs"), + secrets: base.join("secrets"), + }) + } + }, + PathProfile::InteractiveUser => match self.platform { + Platform::Linux | Platform::Macos => { + let base = self + .environment + .home_dir + .as_deref() + .ok_or_else(|| anyhow::anyhow!("HOME is required"))? + .join(".radroots"); + Ok(RuntimeRoots::from_base(base.as_path())) + } + Platform::Windows => { + let roaming = self + .environment + .appdata_dir + .as_deref() + .ok_or_else(|| anyhow::anyhow!("APPDATA is required"))? + .join("Radroots"); + let local = self + .environment + .localappdata_dir + .as_deref() + .ok_or_else(|| anyhow::anyhow!("LOCALAPPDATA is required"))? + .join("Radroots"); + Ok(RuntimeRoots { + config: roaming.join("config"), + data: local.join("data"), + logs: local.join("logs"), + secrets: roaming.join("secrets"), + }) + } + }, + } + } +} + +#[derive(Debug, Clone)] +struct RuntimeRoots { + config: PathBuf, + data: PathBuf, + logs: PathBuf, + secrets: PathBuf, +} + +impl RuntimeRoots { + fn from_base(base: &Path) -> Self { + Self { + config: base.join("config"), + data: base.join("data"), + logs: base.join("logs"), + secrets: base.join("secrets"), + } + } + + fn service(self) -> Self { + Self { + config: self.config.join("services").join(RADROOTSD_RUNTIME_ID), + data: self.data.join("services").join(RADROOTSD_RUNTIME_ID), + logs: self.logs.join("services").join(RADROOTSD_RUNTIME_ID), + secrets: self.secrets.join("services").join(RADROOTSD_RUNTIME_ID), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct RuntimePathSelection { + pub(crate) profile: PathProfile, + pub(crate) repo_local_root: Option<PathBuf>, + profile_source: String, + repo_local_root_source: Option<String>, +} + +impl RuntimePathSelection { + #[cfg(test)] + pub(crate) fn caller(profile: PathProfile, repo_local_root: Option<PathBuf>) -> Self { + Self { + profile, + repo_local_root, + profile_source: "caller".to_owned(), + repo_local_root_source: None, + } + } + + fn from_env() -> Result<Self> { + let profile_value = std::env::var(RADROOTSD_PATHS_PROFILE_ENV).ok(); + let profile = profile_value + .as_deref() + .map(PathProfile::parse) + .transpose()? + .unwrap_or(PathProfile::InteractiveUser); + let repo_local_root = std::env::var_os(RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV) + .filter(|value| !value.is_empty()) + .map(PathBuf::from); + if profile == PathProfile::RepoLocal && repo_local_root.is_none() { + bail!("{RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV} is required for repo_local"); + } + Ok(Self { + profile, + repo_local_root, + profile_source: if profile_value.is_some() { + RADROOTSD_PATHS_PROFILE_ENV.to_owned() + } else { + "default".to_owned() + }, + repo_local_root_source: std::env::var_os(RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV) + .map(|_| RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV.to_owned()), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct RadrootsdRuntimePathOverrideContractOutput { + pub profile_source: String, + pub root_source: String, + pub repo_local_root: Option<PathBuf>, + pub repo_local_root_source: Option<String>, + pub subordinate_path_override_source: String, + pub subordinate_path_override_keys: Vec<String>, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RadrootsdRuntimePaths { pub(crate) config_path: PathBuf, @@ -42,49 +262,29 @@ pub struct RadrootsdRuntimeContractOutput { pub canonical_transport_publish_database_path: PathBuf, } -pub type RadrootsdRuntimePathOverrideContractOutput = RadrootsRuntimeSelectionOverrideContract; - -pub(crate) fn process_path_selection() -> Result<(RadrootsPathProfile, Option<PathBuf>)> { - let selection = process_path_selection_with_sources()?; +pub(crate) fn process_path_selection() -> Result<(PathProfile, Option<PathBuf>)> { + let selection = RuntimePathSelection::from_env()?; Ok((selection.profile, selection.repo_local_root)) } -fn process_path_selection_with_sources() -> Result<RadrootsRuntimePathSelection> { - RadrootsRuntimePathSelection::from_env( - RADROOTSD_PATHS_PROFILE_ENV, - RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV, - RadrootsPathProfile::InteractiveUser, - ) - .map_err(|error| anyhow::anyhow!(error.to_string())) -} - pub(crate) fn resolve_runtime_paths_with_resolver( - resolver: &RadrootsPathResolver, - profile: RadrootsPathProfile, + resolver: &PathResolver, + profile: PathProfile, repo_local_root: Option<&Path>, ) -> Result<RadrootsdRuntimePaths> { - let selection = - RadrootsRuntimePathSelection::caller(profile, repo_local_root.map(Path::to_path_buf)); - let namespaced = selection - .resolve_service_roots( - resolver, - RADROOTSD_RUNTIME_ID, - RADROOTSD_PATHS_PROFILE_ENV, - RADROOTSD_PATHS_REPO_LOCAL_ROOT_ENV, - ) - .map_err(|error| anyhow::anyhow!("resolve radrootsd runtime paths: {error}"))?; + let roots = resolver.roots(profile, repo_local_root)?.service(); Ok(RadrootsdRuntimePaths { - config_path: namespaced.config.join(DEFAULT_CONFIG_FILE_NAME), - logs_dir: namespaced.logs, - identity_path: namespaced.secrets.join(DEFAULT_SERVICE_IDENTITY_FILE_NAME), - transport_publish_database_path: namespaced.data.join(TRANSPORT_PUBLISH_DATABASE_FILE_NAME), + config_path: roots.config.join(DEFAULT_CONFIG_FILE_NAME), + logs_dir: roots.logs, + identity_path: roots.secrets.join(DEFAULT_SERVICE_IDENTITY_FILE_NAME), + transport_publish_database_path: roots.data.join(TRANSPORT_PUBLISH_DATABASE_FILE_NAME), }) } pub(crate) fn default_runtime_paths_for_process() -> Result<RadrootsdRuntimePaths> { let (profile, repo_local_root) = process_path_selection()?; resolve_runtime_paths_with_resolver( - &RadrootsPathResolver::current(), + &PathResolver::current(), profile, repo_local_root.as_deref(), ) @@ -105,26 +305,40 @@ pub fn default_identity_path_for_process() -> Result<PathBuf> { } pub fn runtime_contract_for_process() -> Result<RadrootsdRuntimeContractOutput> { - let selection = process_path_selection_with_sources()?; - runtime_contract_with_selection(&RadrootsPathResolver::current(), &selection) + let selection = RuntimePathSelection::from_env()?; + runtime_contract_with_selection(&PathResolver::current(), &selection) } pub(crate) fn runtime_contract_with_selection( - resolver: &RadrootsPathResolver, - selection: &RadrootsRuntimePathSelection, + resolver: &PathResolver, + selection: &RuntimePathSelection, ) -> Result<RadrootsdRuntimeContractOutput> { - let profile = selection.profile; - let repo_local_root = selection.repo_local_root.as_deref(); - let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?; - let base_contract: RadrootsRuntimeSelectionContract = selection.contract( - &RADROOTSD_ALLOWED_PROFILES, - SUBORDINATE_PATH_OVERRIDE_SOURCE, - &SUBORDINATE_PATH_OVERRIDE_KEYS, - ); + let paths = resolve_runtime_paths_with_resolver( + resolver, + selection.profile, + selection.repo_local_root.as_deref(), + )?; Ok(RadrootsdRuntimeContractOutput { - active_profile: base_contract.active_profile, - allowed_profiles: base_contract.allowed_profiles, - path_overrides: base_contract.path_overrides, + active_profile: selection.profile.as_str().to_owned(), + allowed_profiles: RADROOTSD_ALLOWED_PROFILES + .into_iter() + .map(str::to_owned) + .collect(), + path_overrides: RadrootsdRuntimePathOverrideContractOutput { + profile_source: selection.profile_source.clone(), + root_source: if selection.profile == PathProfile::RepoLocal { + "explicit_repo_local_root".to_owned() + } else { + "host_defaults".to_owned() + }, + repo_local_root: selection.repo_local_root.clone(), + repo_local_root_source: selection.repo_local_root_source.clone(), + subordinate_path_override_source: SUBORDINATE_PATH_OVERRIDE_SOURCE.to_owned(), + subordinate_path_override_keys: SUBORDINATE_PATH_OVERRIDE_KEYS + .into_iter() + .map(str::to_owned) + .collect(), + }, default_shared_secret_backend: RADROOTSD_DEFAULT_SHARED_SECRET_BACKEND.to_owned(), allowed_shared_secret_backends: RADROOTSD_ALLOWED_SHARED_SECRET_BACKENDS .into_iter() @@ -141,33 +355,18 @@ pub(crate) fn runtime_contract_with_selection( mod tests { use std::path::PathBuf; - use radroots_runtime_paths::{ - RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, - RadrootsRuntimePathSelection, - }; - use super::{ - RadrootsdRuntimeContractOutput, default_config_path_for_process, - runtime_contract_for_process, runtime_contract_with_selection, + HostEnvironment, PathProfile, PathResolver, Platform, RadrootsdRuntimeContractOutput, + RuntimePathSelection, default_config_path_for_process, runtime_contract_for_process, + runtime_contract_with_selection, }; - fn runtime_contract_with_resolver( - resolver: &RadrootsPathResolver, - profile: RadrootsPathProfile, - repo_local_root: Option<&std::path::Path>, - ) -> anyhow::Result<RadrootsdRuntimeContractOutput> { - runtime_contract_with_selection( - resolver, - &RadrootsRuntimePathSelection::caller(profile, repo_local_root.map(PathBuf::from)), - ) - } - - fn linux_resolver() -> RadrootsPathResolver { - RadrootsPathResolver::new( - RadrootsPlatform::Linux, - RadrootsHostEnvironment { + fn linux_resolver() -> PathResolver { + PathResolver::new( + Platform::Linux, + HostEnvironment { home_dir: Some(PathBuf::from("/home/treesap")), - ..RadrootsHostEnvironment::default() + ..HostEnvironment::default() }, ) } @@ -181,10 +380,9 @@ mod tests { #[test] fn runtime_contract_output_contains_canonical_runtime_paths() { - let contract = runtime_contract_with_resolver( + let contract = runtime_contract_with_selection( &linux_resolver(), - RadrootsPathProfile::InteractiveUser, - None, + &RuntimePathSelection::caller(PathProfile::InteractiveUser, None), ) .expect("contract"); diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -3,6 +3,8 @@ use anyhow::{Context, Result, bail}; use jsonrpsee::server::ServerHandle; use std::time::Duration; use tracing::{info, warn}; +#[cfg(not(test))] +use tracing_subscriber::EnvFilter; use crate::app::identity_storage::load_service_identity; use crate::app::{cli, config, paths}; @@ -49,6 +51,52 @@ enum RunWaitOutcome { Stopped, } +#[cfg(not(test))] +fn init_logging(logs_dir: &std::path::Path, default_level: Option<&str>) -> Result<()> { + std::fs::create_dir_all(logs_dir) + .with_context(|| format!("create log directory {}", logs_dir.display()))?; + let appender = tracing_appender::rolling::never(logs_dir, "radrootsd.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 = std::env::var("RADROOTS_LOG_LEVEL") + .or_else(|_| std::env::var("RUST_LOG")) + .ok() + .or_else(|| default_level.map(str::to_owned)) + .unwrap_or_else(|| "info".to_owned()); + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::new(filter)) + .with_writer(writer) + .try_init() + .map_err(|error| anyhow::anyhow!("initialize logging: {error}"))?; + LOG_GUARD + .set(guard) + .map_err(|_| anyhow::anyhow!("logging is already initialized"))?; + Ok(()) +} + +#[cfg(not(test))] +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } +} + #[derive(Debug, Clone, PartialEq, Eq)] struct RadrootsdRuntimeStartupReport { active_profile: String, @@ -137,7 +185,7 @@ fn load_args_and_settings() -> Result<(cli::Args, config::Settings)> { #[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 @@ -146,7 +194,7 @@ fn load_args_and_settings() -> Result<(cli::Args, config::Settings)> { .unwrap_or_else(paths::default_config_path_for_process)?; let settings = config::load_settings_from_path(&config_path).context("load configuration")?; - radroots_runtime::init_with_logs_dir( + init_logging( std::path::Path::new(settings.config.service.logs_dir.as_str()), None, )?; @@ -325,7 +373,7 @@ async fn publish_service_presence( identity: DaemonIdentity, profile: AuthoredProfile, metadata: crate::host_nostr::Metadata, - service_cfg: radroots_runtime::RadrootsNostrServiceConfig, + service_cfg: config::NostrServiceConfig, nip46_config: config::Nip46Config, ) -> Result<()> { let kinds = service_presence_kinds(); @@ -348,7 +396,7 @@ async fn maybe_publish_service_presence( identity: DaemonIdentity, profile: AuthoredProfile, metadata: crate::host_nostr::Metadata, - service_cfg: radroots_runtime::RadrootsNostrServiceConfig, + service_cfg: config::NostrServiceConfig, nip46_config: config::Nip46Config, ) { #[cfg(test)] @@ -432,7 +480,7 @@ async fn wait_for_shutdown_or_stopped(handle: ServerHandle) -> RunWaitOutcome { #[cfg_attr(coverage_nightly, coverage(off))] async fn wait_for_shutdown_or_stopped(handle: ServerHandle) -> RunWaitOutcome { tokio::select! { - _ = radroots_runtime::shutdown_signal() => RunWaitOutcome::Shutdown, + _ = shutdown_signal() => RunWaitOutcome::Shutdown, _ = handle.stopped() => RunWaitOutcome::Stopped, } } @@ -618,7 +666,7 @@ mod tests { fn args_for_identity(path: PathBuf, allow_generate: bool) -> cli::Args { cli::Args { - service: radroots_runtime::RadrootsServiceCliArgs { + service: cli::ServiceCliArgs { config: Some(PathBuf::from("config.toml")), identity: Some(path), allow_generate_identity: allow_generate, @@ -633,7 +681,7 @@ mod tests { config::Settings { metadata, config: config::Configuration { - service: radroots_runtime::RadrootsNostrServiceConfig { + service: config::NostrServiceConfig { logs_dir: "logs".to_string(), relays, nip89_identifier: Some("radrootsd".to_string()), @@ -946,7 +994,7 @@ mod tests { #[test] fn runtime_startup_report_prefers_explicit_cli_paths() { let args = cli::Args { - service: radroots_runtime::RadrootsServiceCliArgs { + service: cli::ServiceCliArgs { config: Some(PathBuf::from("/tmp/radrootsd/config.toml")), identity: Some(PathBuf::from("/tmp/radrootsd/identity.secret.json")), allow_generate_identity: false, @@ -997,7 +1045,7 @@ mod tests { #[test] fn runtime_startup_report_falls_back_to_canonical_contract_paths() { let args = cli::Args { - service: radroots_runtime::RadrootsServiceCliArgs { + service: cli::ServiceCliArgs { config: None, identity: None, allow_generate_identity: false, diff --git a/tests/source_boundary.rs b/tests/source_boundary.rs @@ -197,13 +197,21 @@ fn transport_publish_sources_reject_removed_protocol_identifiers() { } } assert!( - manifest_source.contains("radroots_transport_nostr = { path = "), - "Cargo.toml must depend on radroots_transport_nostr directly" + manifest_source.contains("radroots_transport_nostr = { version = \"=0.1.0-alpha\""), + "Cargo.toml must depend on the exact radroots_transport_nostr registry version" ); assert!( !manifest_source.contains("package = \"radroots_transport_nostr\""), "Cargo.toml must not disguise radroots_transport_nostr through a package alias" ); + assert!( + !manifest_source.contains("path = \"../lib"), + "Cargo.toml must not retain sibling source dependencies" + ); + assert!( + !manifest_source.contains("radroots_runtime"), + "daemon lifecycle and path policy must remain host-owned" + ); assert!( findings.is_empty(), @@ -261,20 +269,26 @@ fn foundation_hardening_sources_reject_retired_names_and_ambiguous_docs() { #[test] fn transport_publish_sources_reject_removed_execution_transport_targets() { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let protocol_source = read_source( - manifest_dir - .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") - .as_path(), + use radroots_protocol::radrootsd::transport_publish::v5::{ + DeliveryPolicy, Error, EventRequest, Target, TargetPolicy, + }; + let request = EventRequest { + raw_event_json: "{}".to_owned(), + target_policy: TargetPolicy::explicit_targets(vec![Target { + transport_kind: "proxy".to_owned(), + endpoint_uri: "proxy:publish".to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: None, + }]), + delivery_policy: DeliveryPolicy::Any, + idempotency_key: None, + timeout_ms: None, + }; + assert_eq!( + request.validate(20), + Err(Error::InvalidTransportKind { index: 0 }) ); - for required in [ - "\"local\" | \"nostr\" | \"reticulum\" => {}", - "_ => return Err(Error::InvalidTransportKind { index })", - ] { - assert!( - protocol_source.contains(required), - "transport publish protocol must retain removed transport kind rejection witness `{required}`" - ); - } let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); for required in [ @@ -369,12 +383,6 @@ fn transport_publish_store_egress_requires_protocol_validation() { fn transport_publish_required_targets_stay_fingerprint_exact() { let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let daemon_source = read_source(manifest_dir.join("src/core/transport_publish.rs").as_path()); - let protocol_source = read_source( - manifest_dir - .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") - .as_path(), - ); - for required in [ "validate_delivery_policy_for_resolution", "let target_fingerprints = resolution.target_fingerprints()?;", @@ -418,16 +426,10 @@ fn transport_publish_required_targets_stay_fingerprint_exact() { ); } - for required in [ - "RequiredTargetNotInTargetSet", - "Matching fingerprints to native targets is intentionally deferred", - "Self::RequiredTargets { targets } => targets.len()", - ] { - assert!( - protocol_source.contains(required), - "transport publish protocol must retain exact required-target witness `{required}`" - ); - } + use radroots_protocol::radrootsd::transport_publish::v5::{DeliveryPolicy, TargetFingerprint}; + let target = TargetFingerprint::parse("a".repeat(64)).expect("fingerprint"); + let policy = DeliveryPolicy::required_targets(vec![target]).expect("required targets"); + assert_eq!(policy.required_target_count(9), 1); } #[test] @@ -461,44 +463,21 @@ fn transport_publish_capabilities_expose_per_transport_readiness() { ); } - let protocol_source = read_source( - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") - .as_path(), - ); - for required in [ - "pub transport: String,", - "pub configured: bool,", - "pub implementation: Implementation,", - "pub usable_for_delivery: bool,", - "pub capabilities: OperationCapabilities,", - "pub struct OperationCapabilities", - "pub deliver: bool,", - "pub fetch: bool,", - "pub discovery: bool,", - "pub gateway_forwarding: bool,", - "pub receipt_observation: bool,", - "RETICULUM_UNAVAILABLE_MESSAGE", - ] { - assert!( - protocol_source.contains(required), - "transport publish protocol must retain capability field `{required}`" - ); - } - let capability_source = source_window( - protocol_source.as_str(), - "pub struct TransportCapability", - "#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\n#[cfg_attr(feature = \"serde\", serde(rename_all = \"snake_case\"))]\n#[derive(Clone, Copy, Debug, PartialEq, Eq)]\npub enum DeliveryPolicyName", - ); - for forbidden in [ - "pub transport_kind: String,", - concat!("pub implementation", "_state: ImplementationState,"), - ] { - assert!( - !capability_source.contains(forbidden), - "transport publish capability rows must not retain removed field `{forbidden}`" - ); - } + use radroots_protocol::radrootsd::transport_publish::v5::{ + Capabilities, Implementation, RETICULUM_UNAVAILABLE_MESSAGE, + }; + let capabilities = Capabilities::v5(1024, 20); + let reticulum = capabilities + .publish + .transports + .iter() + .find(|transport| transport.transport == "reticulum") + .expect("reticulum capability"); + assert!(reticulum.configured); + assert_eq!(reticulum.implementation, Implementation::Real); + assert!(!reticulum.usable_for_delivery); + assert!(!reticulum.capabilities.deliver); + assert_eq!(reticulum.message, RETICULUM_UNAVAILABLE_MESSAGE); } #[test]