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:
| M | Cargo.lock | | | 483 | ++++++------------------------------------------------------------------------- |
| M | Cargo.toml | | | 26 | ++++++++++++++------------ |
| M | src/app/cli.rs | | | 29 | ++++++++++++++++++++++++++--- |
| M | src/app/config.rs | | | 77 | ++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
| M | src/app/identity_storage.rs | | | 220 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
| M | src/app/paths.rs | | | 352 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ |
| M | src/app/runtime.rs | | | 66 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- |
| M | tests/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]