radrootsd

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

commit a854579e00d43d8741f3afeba80946811ac06c51
parent 224f8f709ce49d84ef0c078403cd81822cf1b0e6
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 18:08:44 +0000

radrootsd: migrate to final protocol and transport crates

- adopt the versioned V5 daemon protocol DTO surface
- keep identity custody and relay client adaptation daemon-owned
- align lower-layer crate versions and the Rust toolchain
- cover final transport, RPC, storage, and source boundaries

Diffstat:
MCargo.lock | 519+++++++++++++++++++++++++++----------------------------------------------------
MCargo.toml | 33+++++++++++++++++----------------
Mrust-toolchain.toml | 2+-
Msrc/app/config.rs | 24++++++++++++------------
Msrc/app/identity_storage.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/app/mod.rs | 2+-
Msrc/app/runtime.rs | 121+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/core/nip46/session.rs | 46+++++++++++++++++++++++-----------------------
Msrc/core/state.rs | 32+++++++++++++++-----------------
Msrc/core/transport_publish.rs | 1402++++++++++++++++++++++++++++++++++---------------------------------------------
Asrc/host_nostr.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 1+
Msrc/transport/jsonrpc/auth.rs | 10++++------
Msrc/transport/jsonrpc/methods/mod.rs | 14+++++++-------
Msrc/transport/jsonrpc/methods/nip46/connect.rs | 86+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/transport/jsonrpc/methods/nip46/get_public_key.rs | 2+-
Msrc/transport/jsonrpc/methods/nip46/nip04.rs | 4++--
Msrc/transport/jsonrpc/methods/nip46/nip44.rs | 4++--
Msrc/transport/jsonrpc/methods/transport_publish.rs | 36++++++++++++++++--------------------
Msrc/transport/jsonrpc/nip46/client.rs | 31+++++++++++++++----------------
Msrc/transport/jsonrpc/nip46/connection.rs | 4++--
Msrc/transport/jsonrpc/server.rs | 30++++++++++++++----------------
Msrc/transport/mod.rs | 1+
Msrc/transport/nostr/listener.rs | 23++++++++++-------------
Msrc/transport/nostr/protocol.rs | 14+++++++-------
Asrc/transport/relay_publish.rs | 333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/source_boundary.rs | 49+++++++++++++++++++++++++------------------------
27 files changed, 1582 insertions(+), 1456 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -197,6 +197,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -313,12 +319,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] name = "chacha20" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -441,6 +441,12 @@ dependencies = [ ] [[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" @@ -530,6 +536,18 @@ 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" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -547,6 +565,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -602,6 +630,23 @@ dependencies = [ ] [[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -644,6 +689,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -695,7 +750,6 @@ checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -794,6 +848,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -816,11 +871,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -849,6 +902,17 @@ dependencies = [ ] [[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] name = "h2" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1010,24 +1074,6 @@ dependencies = [ "pin-utils", "smallvec", "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots 1.0.6", ] [[package]] @@ -1036,21 +1082,13 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", "bytes", - "futures-channel", - "futures-util", "http", "http-body", "hyper", - "ipnet", - "libc", - "percent-encoding", "pin-project-lite", - "socket2", "tokio", "tower-service", - "tracing", ] [[package]] @@ -1166,6 +1204,16 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" @@ -1220,22 +1268,6 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1351,6 +1383,16 @@ dependencies = [ ] [[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", +] + +[[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1413,12 +1455,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] name = "matchers" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1495,6 +1531,7 @@ dependencies = [ "serde_json", "unicode-normalization", "url", + "url-fork", ] [[package]] @@ -1800,61 +1837,6 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1877,7 +1859,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_blossom" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "mediatype", "serde", @@ -1888,22 +1870,22 @@ dependencies = [ [[package]] name = "radroots_core" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "rust_decimal", - "rust_decimal_macros", "serde", ] [[package]] name = "radroots_event" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "hex", "jiff-tzdb", "radroots_blossom", "radroots_core", - "secp256k1", + "radroots_identity", + "radroots_protocol", "serde", "serde_json", "sha2", @@ -1913,34 +1895,30 @@ dependencies = [ [[package]] name = "radroots_event_codec" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "nostr", "radroots_blossom", "radroots_core", "radroots_event", + "radroots_identity", + "radroots_protocol", + "secp256k1", "serde", "serde_json", ] [[package]] name = "radroots_identity" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "nostr", - "radroots_protected_store", - "radroots_runtime", - "radroots_runtime_paths", - "radroots_secret_vault", + "k256", "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", + "thiserror 2.0.18", ] [[package]] name = "radroots_log" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "chrono", "serde_json", @@ -1952,14 +1930,12 @@ dependencies = [ [[package]] name = "radroots_nostr" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "nostr", - "nostr-sdk", "radroots_event", "radroots_event_codec", "radroots_identity", - "reqwest", "serde", "serde_json", "thiserror 1.0.69", @@ -1967,7 +1943,7 @@ dependencies = [ [[package]] name = "radroots_protected_store" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "chacha20poly1305", "getrandom 0.2.17", @@ -1978,8 +1954,15 @@ dependencies = [ ] [[package]] +name = "radroots_protocol" +version = "0.1.0-alpha" +dependencies = [ + "serde", +] + +[[package]] name = "radroots_runtime" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "anyhow", "chacha20poly1305", @@ -2002,7 +1985,7 @@ dependencies = [ [[package]] name = "radroots_runtime_paths" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ "serde", "thiserror 1.0.69", @@ -2010,40 +1993,36 @@ dependencies = [ [[package]] name = "radroots_secret_vault" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" [[package]] name = "radroots_transport" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "serde", + "radroots_event", + "radroots_identity", + "radroots_protocol", "sha2", ] [[package]] name = "radroots_transport_nostr" -version = "1.0.0-alpha.1" +version = "0.1.0-alpha" dependencies = [ - "futures", - "nostr", - "radroots_event", + "async-wsocket", + "nostr-relay-pool", + "nostr-sdk", + "radroots_event_codec", "radroots_nostr", + "radroots_protocol", "radroots_transport", - "serde", "serde_json", - "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", "url", ] [[package]] -name = "radroots_transport_publish_protocol" -version = "1.0.0-alpha.1" -dependencies = [ - "radroots_transport", - "serde", -] - -[[package]] name = "radrootsd" version = "0.1.0" dependencies = [ @@ -2052,15 +2031,16 @@ dependencies = [ "futures-executor", "jsonrpsee", "nostr", + "nostr-sdk", "radroots_event", "radroots_event_codec", "radroots_identity", "radroots_nostr", + "radroots_protocol", "radroots_runtime", "radroots_runtime_paths", "radroots_transport", "radroots_transport_nostr", - "radroots_transport_publish_protocol", "rand 0.9.2", "serde", "serde_json", @@ -2162,44 +2142,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.6", -] - -[[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2253,16 +2195,6 @@ dependencies = [ ] [[package]] -name = "rust_decimal_macros" -version = "1.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" -dependencies = [ - "quote", - "syn", -] - -[[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2301,7 +2233,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] @@ -2356,6 +2287,19 @@ dependencies = [ ] [[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] name = "secp256k1" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2446,18 +2390,6 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2686,9 +2618,6 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] [[package]] name = "synstructure" @@ -2971,25 +2900,6 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", "tower-layer", "tower-service", ] @@ -3081,12 +2991,6 @@ dependencies = [ ] [[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] name = "tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3118,6 +3022,12 @@ 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" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] name = "unicode-general-category" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3173,13 +3083,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", - "idna", + "idna 1.1.0", "percent-encoding", "serde", "serde_derive", ] [[package]] +name = "url-fork" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa3323c39b8e786154d3000b70ae9af0e9bd746c9791456da0d4a1f68ad89d6" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", + "serde", +] + +[[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3227,15 +3149,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3363,16 +3276,6 @@ dependencies = [ ] [[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3455,16 +3358,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3482,31 +3376,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3516,96 +3393,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -3,38 +3,39 @@ name = "radrootsd" version = "0.1.0" edition = "2024" authors = ["Radroots Authors"] -rust-version = "1.97.0" +rust-version = "1.97.1" license = "AGPL-3.0-or-later" description = "Radroots local runtime daemon for storage, sync, and relay publishing" [workspace] -resolver = "2" +resolver = "3" [workspace.dependencies] -radroots_event = { path = "../lib/crates/event", version = "=1.0.0-alpha.1" } -radroots_event_codec = { path = "../lib/crates/event_codec", version = "=1.0.0-alpha.1" } -radroots_identity = { path = "../lib/crates/identity", version = "=1.0.0-alpha.1" } -radroots_nostr = { path = "../lib/crates/nostr", version = "=1.0.0-alpha.1" } -radroots_transport_publish_protocol = { path = "../lib/crates/transport_publish_protocol", version = "=1.0.0-alpha.1" } -radroots_transport_nostr = { path = "../lib/crates/transport_nostr", version = "=1.0.0-alpha.1", default-features = false } -radroots_transport = { path = "../lib/crates/transport", version = "=1.0.0-alpha.1", default-features = false } -radroots_runtime = { path = "../lib/crates/runtime", version = "=1.0.0-alpha.1" } -radroots_runtime_paths = { path = "../lib/crates/runtime_paths", version = "=1.0.0-alpha.1" } +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" } [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] -radroots_event = { workspace = true, features = ["serde", "signature"] } -radroots_event_codec = { workspace = true, features = ["serde_json"] } +radroots_event = { workspace = true, features = ["serde"] } +radroots_event_codec = { workspace = true, features = ["json"] } radroots_identity = { workspace = true } -radroots_nostr = { workspace = true, features = ["client", "codec", "events", "http"] } -radroots_transport_publish_protocol = { workspace = true, features = ["std", "serde"] } -radroots_transport_nostr = { workspace = true, features = ["std", "client"] } +radroots_nostr = { workspace = true, features = ["events"] } +radroots_protocol = { workspace = true, features = ["std", "serde"] } +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" } clap = { version = "4", features = ["derive"] } diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.97.0" +channel = "1.97.1" diff --git a/src/app/config.rs b/src/app/config.rs @@ -1,6 +1,6 @@ +use crate::host_nostr::Metadata; use anyhow::{Context, Result, bail}; -use radroots_event::profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier}; -use radroots_nostr::prelude::RadrootsNostrMetadata; +use radroots_event::profile::{AuthoredProfile, Nip05Identifier}; use radroots_runtime::RadrootsNostrServiceConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -176,7 +176,7 @@ struct RawConfiguration { #[derive(Debug, Deserialize, Clone)] struct RawSettings { - pub metadata: RadrootsNostrMetadata, + pub metadata: Metadata, pub config: RawConfiguration, } @@ -390,18 +390,18 @@ impl Configuration { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { - pub metadata: RadrootsNostrMetadata, + pub metadata: Metadata, pub config: Configuration, } impl Settings { - pub fn authored_profile(&self) -> Result<RadrootsAuthoredProfile> { + pub fn authored_profile(&self) -> Result<AuthoredProfile> { let metadata = &self.metadata; let name = metadata .name .as_deref() .ok_or_else(|| anyhow::anyhow!("metadata.name is required for the authored Profile"))?; - let mut profile = RadrootsAuthoredProfile::new(name.to_owned()) + let mut profile = AuthoredProfile::new(name.to_owned()) .context("metadata.name is invalid for the authored Profile")?; if let Some(display_name) = metadata.display_name.as_ref() { @@ -411,7 +411,7 @@ impl Settings { profile = profile.with_about(about.clone()); } if let Some(nip05) = metadata.nip05.as_deref() { - let nip05 = RadrootsNip05Identifier::parse(nip05) + let nip05 = Nip05Identifier::parse(nip05) .context("metadata.nip05 is invalid for the authored Profile")?; profile = profile.with_nip05(nip05); } @@ -485,7 +485,7 @@ mod tests { RadrootsdRuntimeContractOutput, default_runtime_paths_for_process, resolve_runtime_paths_with_resolver, runtime_contract_with_selection, }; - use radroots_event::profile::RadrootsNip05Identifier; + use radroots_event::profile::Nip05Identifier; use radroots_runtime::RadrootsNostrServiceConfig; use radroots_runtime_paths::{ RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, @@ -609,7 +609,7 @@ mod tests { #[test] fn authored_profile_accepts_only_strict_metadata_fields() { - let mut metadata = radroots_nostr::prelude::RadrootsNostrMetadata::new() + let mut metadata = crate::host_nostr::Metadata::new() .name("radrootsd") .display_name("Radroots daemon") .about("local relay publishing") @@ -631,7 +631,7 @@ mod tests { assert_eq!(profile.display_name(), Some("Radroots daemon")); assert_eq!(profile.about(), Some("local relay publishing")); assert_eq!( - profile.nip05().map(RadrootsNip05Identifier::as_str), + profile.nip05().map(Nip05Identifier::as_str), Some("daemon@radroots.example") ); assert_eq!(profile.bot(), Some(true)); @@ -641,7 +641,7 @@ mod tests { #[test] fn authored_profile_rejects_url_only_media() { - let metadata = radroots_nostr::prelude::RadrootsNostrMetadata::new() + let metadata = crate::host_nostr::Metadata::new() .name("radrootsd") .picture(url::Url::parse("https://blossom.example/blob").expect("picture URL")); let settings = super::Settings { @@ -668,7 +668,7 @@ mod tests { #[test] fn authored_profile_rejects_missing_invalid_and_unsupported_fields() { let mut settings = super::Settings { - metadata: radroots_nostr::prelude::RadrootsNostrMetadata::new(), + metadata: crate::host_nostr::Metadata::new(), config: Configuration { service: service_config(), rpc: RpcConfig::default(), diff --git a/src/app/identity_storage.rs b/src/app/identity_storage.rs @@ -1,42 +1,126 @@ use std::path::{Path, PathBuf}; -use anyhow::Result; -use radroots_identity::{IdentityError, RadrootsIdentity, RadrootsIdentityFile}; +use anyhow::{Result, bail}; +use nostr::{Keys, SecretKey}; +use serde::{Deserialize, Serialize}; const RADROOTSD_IDENTITY_KEY_SLOT: &str = "radrootsd_identity"; +/// Host-private service signing identity. +/// +/// Public identity values cross package boundaries through +/// `radroots_identity`; secret-key generation and custody remain daemon-owned. +#[derive(Clone)] +pub(crate) struct DaemonIdentity { + keys: Keys, +} + +impl core::fmt::Debug for DaemonIdentity { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("DaemonIdentity") + .field("public_key", &self.public_key_hex()) + .finish_non_exhaustive() + } +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct DaemonIdentityFile { + secret_key: String, + #[serde(skip_serializing_if = "Option::is_none")] + public_key: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + identifier: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option<nostr::Event>, + #[serde(skip_serializing_if = "Option::is_none")] + application_handler: Option<nostr::Event>, +} + +impl DaemonIdentity { + pub(crate) fn generate() -> Self { + Self { + keys: Keys::generate(), + } + } + + pub(crate) const fn keys(&self) -> &Keys { + &self.keys + } + + pub(crate) fn public_key(&self) -> nostr::PublicKey { + self.keys.public_key() + } + + pub(crate) fn public_key_hex(&self) -> String { + self.public_key().to_hex() + } + + #[cfg(test)] + pub(crate) fn id(&self) -> String { + self.public_key_hex() + } + + fn to_file(&self) -> DaemonIdentityFile { + DaemonIdentityFile { + secret_key: self.keys.secret_key().to_secret_hex(), + public_key: Some(self.public_key_hex()), + identifier: None, + metadata: None, + application_handler: None, + } + } + + fn from_file(file: DaemonIdentityFile) -> Result<Self> { + let secret_key = SecretKey::parse(file.secret_key.as_str()) + .map_err(|_| anyhow::anyhow!("invalid daemon identity secret"))?; + let identity = Self { + keys: Keys::new(secret_key), + }; + if file + .public_key + .as_deref() + .is_some_and(|expected| expected != identity.public_key_hex()) + { + bail!("daemon identity public key does not match encrypted secret"); + } + Ok(identity) + } +} + #[cfg(test)] pub fn encrypted_identity_key_path(path: impl AsRef<Path>) -> PathBuf { radroots_runtime::local_wrapping_key_path(path) } -pub fn load_service_identity( - path: Option<&Path>, - allow_generate: bool, -) -> Result<RadrootsIdentity> { +pub fn load_service_identity(path: Option<&Path>, allow_generate: bool) -> Result<DaemonIdentity> { let path = resolved_identity_path(path); if path.exists() { return load_encrypted_identity(&path); } if !allow_generate { - return Err(IdentityError::GenerationNotAllowed(path).into()); + bail!( + "daemon identity generation is not allowed at {}", + path.display() + ); } - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); store_encrypted_identity(&path, &identity)?; Ok(identity) } -pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &RadrootsIdentity) -> Result<()> { +pub fn store_encrypted_identity(path: impl AsRef<Path>, identity: &DaemonIdentity) -> Result<()> { let payload = serde_json::to_vec(&identity.to_file())?; radroots_runtime::seal_local_secret_file(path, RADROOTSD_IDENTITY_KEY_SLOT, &payload)?; Ok(()) } -pub fn load_encrypted_identity(path: impl AsRef<Path>) -> Result<RadrootsIdentity> { +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: RadrootsIdentityFile = serde_json::from_slice(&payload)?; - Ok(RadrootsIdentity::try_from(file)?) + let file: DaemonIdentityFile = serde_json::from_slice(&payload)?; + DaemonIdentity::from_file(file) } fn resolved_identity_path(path: Option<&Path>) -> PathBuf { diff --git a/src/app/mod.rs b/src/app/mod.rs @@ -1,6 +1,6 @@ pub mod cli; pub mod config; -mod identity_storage; +pub(crate) mod identity_storage; mod paths; mod runtime; diff --git a/src/app/runtime.rs b/src/app/runtime.rs @@ -1,6 +1,6 @@ +use crate::app::identity_storage::DaemonIdentity; use anyhow::{Context, Result, bail}; use jsonrpsee::server::ServerHandle; -use radroots_identity::RadrootsIdentity; use std::time::Duration; use tracing::{info, warn}; @@ -12,16 +12,13 @@ use crate::core::transport_publish::{ parse_explicit_transport_kind, parse_nostr_source_policy, parse_target_policy, write_token_file, }; +use crate::host_nostr::{ApplicationHandlerSpec, Kind, build_application_handler, build_profile}; use crate::transport::jsonrpc; #[cfg(not(test))] use crate::transport::nostr::listener::spawn_nip46_listener; #[cfg(not(test))] use clap::Parser; -use radroots_event::profile::RadrootsAuthoredProfile; -use radroots_nostr::prelude::{ - RadrootsNostrApplicationHandlerSpec, RadrootsNostrKind, - radroots_nostr_build_application_handler_event, radroots_nostr_build_profile_event, -}; +use radroots_event::profile::AuthoredProfile; use std::path::PathBuf; #[cfg(test)] @@ -263,10 +260,10 @@ fn log_runtime_startup_report(report: &RadrootsdRuntimeStartupReport) { #[cfg_attr(coverage_nightly, coverage(off))] async fn bootstrap_presence( - client: &radroots_nostr::prelude::RadrootsNostrClient, - identity: &RadrootsIdentity, - profile: &RadrootsAuthoredProfile, - handler_spec: &RadrootsNostrApplicationHandlerSpec, + client: &crate::host_nostr::DaemonNostrClient, + identity: &DaemonIdentity, + profile: &AuthoredProfile, + handler_spec: &ApplicationHandlerSpec, ) -> Result<()> { if let Some(result) = take_bootstrap_hook_result() { return result.map_err(anyhow::Error::msg); @@ -292,15 +289,15 @@ async fn bootstrap_presence( } fn build_service_presence_events( - identity: &RadrootsIdentity, - profile: &RadrootsAuthoredProfile, - handler_spec: &RadrootsNostrApplicationHandlerSpec, + identity: &DaemonIdentity, + profile: &AuthoredProfile, + handler_spec: &ApplicationHandlerSpec, ) -> Result<(nostr::Event, nostr::Event)> { - let profile_event = radroots_nostr_build_profile_event(profile) + let profile_event = build_profile(profile) .context("build service Profile event")? .sign_with_keys(identity.keys()) .context("sign service Profile event")?; - let handler_event = radroots_nostr_build_application_handler_event(handler_spec) + let handler_event = build_application_handler(handler_spec) .context("build NIP-89 application handler event")? .sign_with_keys(identity.keys()) .context("sign NIP-89 application handler event")?; @@ -308,7 +305,7 @@ fn build_service_presence_events( } async fn publish_presence_event( - client: &radroots_nostr::prelude::RadrootsNostrClient, + client: &crate::host_nostr::DaemonNostrClient, event: &nostr::Event, event_name: &str, ) -> Result<()> { @@ -324,31 +321,33 @@ async fn publish_presence_event( #[cfg_attr(coverage_nightly, coverage(off))] async fn publish_service_presence( - client: radroots_nostr::prelude::RadrootsNostrClient, - identity: RadrootsIdentity, - profile: RadrootsAuthoredProfile, - metadata: radroots_nostr::prelude::RadrootsNostrMetadata, + client: crate::host_nostr::DaemonNostrClient, + identity: DaemonIdentity, + profile: AuthoredProfile, + metadata: crate::host_nostr::Metadata, service_cfg: radroots_runtime::RadrootsNostrServiceConfig, nip46_config: config::Nip46Config, ) -> Result<()> { let kinds = service_presence_kinds(); - let handler_spec = RadrootsNostrApplicationHandlerSpec { - kinds, - identifier: service_cfg.nip89_identifier.clone(), - metadata: Some(metadata.clone()), - extra_tags: service_cfg.nip89_extra_tags.clone(), - relays: service_cfg.relays.clone(), - nostrconnect_url: nip46_config.nostrconnect_url.clone(), - }; + let mut handler_spec = ApplicationHandlerSpec::new(kinds) + .with_metadata(metadata.clone()) + .with_extra_tags(service_cfg.nip89_extra_tags.clone()) + .with_relays(service_cfg.relays.clone()); + if let Some(identifier) = service_cfg.nip89_identifier.clone() { + handler_spec = handler_spec.with_identifier(identifier); + } + if let Some(url) = nip46_config.nostrconnect_url.clone() { + handler_spec = handler_spec.with_nostr_connect_url(url); + } bootstrap_presence(&client, &identity, &profile, &handler_spec).await } #[cfg_attr(coverage_nightly, coverage(off))] async fn maybe_publish_service_presence( - client: radroots_nostr::prelude::RadrootsNostrClient, - identity: RadrootsIdentity, - profile: RadrootsAuthoredProfile, - metadata: radroots_nostr::prelude::RadrootsNostrMetadata, + client: crate::host_nostr::DaemonNostrClient, + identity: DaemonIdentity, + profile: AuthoredProfile, + metadata: crate::host_nostr::Metadata, service_cfg: radroots_runtime::RadrootsNostrServiceConfig, nip46_config: config::Nip46Config, ) { @@ -560,7 +559,7 @@ pub async fn run() -> Result<()> { } fn service_presence_kinds() -> Vec<u32> { - let mut kinds = vec![RadrootsNostrKind::NostrConnect.as_u16() as u32]; + let mut kinds = vec![Kind::NostrConnect.as_u16() as u32]; kinds.sort_unstable(); kinds.dedup(); kinds @@ -574,13 +573,11 @@ mod tests { run_bootstrap_hook, run_load_hook, run_start_rpc_hook, run_wait_hook, runtime_startup_report, }; + use crate::app::identity_storage::DaemonIdentity; use crate::app::{cli, config, paths}; use crate::core::Radrootsd; + use crate::host_nostr::{ApplicationHandlerSpec, Kind, Metadata}; use crate::transport::jsonrpc; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::{ - RadrootsNostrApplicationHandlerSpec, RadrootsNostrKind, RadrootsNostrMetadata, - }; use std::path::Path; use std::path::PathBuf; use tokio::sync::{Mutex, MutexGuard}; @@ -631,7 +628,7 @@ mod tests { } fn settings_with_relays(relays: Vec<String>) -> config::Settings { - let metadata: RadrootsNostrMetadata = + let metadata: Metadata = serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata"); config::Settings { metadata, @@ -688,7 +685,7 @@ mod tests { } async fn make_handle(settings: &config::Settings) -> jsonrpsee::server::ServerHandle { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let state = Radrootsd::new( identity, settings.metadata.clone(), @@ -884,33 +881,25 @@ mod tests { fn service_presence_kinds_include_nostr_connect_only() { let kinds = super::service_presence_kinds(); - assert!( - kinds.contains( - &(radroots_nostr::prelude::RadrootsNostrKind::NostrConnect.as_u16() as u32) - ) - ); + assert!(kinds.contains(&(crate::host_nostr::Kind::NostrConnect.as_u16() as u32))); assert_eq!(kinds.len(), 1); } #[test] fn service_presence_events_use_strict_profile_and_nip89_contracts() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let settings = settings_with_relays(vec!["wss://relay.example.com".to_owned()]); let profile = settings.authored_profile().expect("authored profile"); - let handler_spec = RadrootsNostrApplicationHandlerSpec { - kinds: super::service_presence_kinds(), - identifier: Some("radrootsd".to_owned()), - metadata: Some(settings.metadata), - extra_tags: Vec::new(), - relays: settings.config.service.relays, - nostrconnect_url: None, - }; + let handler_spec = ApplicationHandlerSpec::new(super::service_presence_kinds()) + .with_identifier("radrootsd") + .with_metadata(settings.metadata) + .with_relays(settings.config.service.relays); let (profile_event, handler_event) = build_service_presence_events(&identity, &profile, &handler_spec) .expect("service presence events"); - assert_eq!(profile_event.kind, RadrootsNostrKind::Metadata); + assert_eq!(profile_event.kind, Kind::Metadata); assert!(profile_event.tags.is_empty()); assert_eq!(profile_event.pubkey, identity.public_key()); assert_eq!( @@ -920,7 +909,7 @@ mod tests { ); assert_eq!( handler_event.kind, - RadrootsNostrKind::Custom(radroots_event::kinds::KIND_APPLICATION_HANDLER as u16) + Kind::Custom(radroots_event::envelope::kind::KIND_APPLICATION_HANDLER as u16) ); assert_eq!(handler_event.pubkey, identity.public_key()); assert!( @@ -929,12 +918,7 @@ mod tests { }) ); assert!(handler_event.tags.iter().any(|tag| { - tag.as_slice() - == [ - "k".to_owned(), - RadrootsNostrKind::NostrConnect.as_u16().to_string(), - ] - .as_slice() + tag.as_slice() == ["k".to_owned(), Kind::NostrConnect.as_u16().to_string()].as_slice() })); assert!(handler_event.tags.iter().any(|tag| { tag.as_slice() == ["relay".to_owned(), "wss://relay.example.com".to_owned()].as_slice() @@ -945,18 +929,13 @@ mod tests { #[tokio::test] async fn bootstrap_presence_fails_closed_without_a_connected_relay() { let _guard = test_guard().await; - let identity = RadrootsIdentity::generate(); - let client = radroots_nostr::prelude::RadrootsNostrClient::from_identity(&identity); + let identity = DaemonIdentity::generate(); + let client = crate::host_nostr::DaemonNostrClient::from_identity(&identity); let settings = settings_with_relays(Vec::new()); let profile = settings.authored_profile().expect("authored profile"); - let handler_spec = RadrootsNostrApplicationHandlerSpec { - kinds: super::service_presence_kinds(), - identifier: Some("radrootsd".to_owned()), - metadata: Some(settings.metadata), - extra_tags: Vec::new(), - relays: Vec::new(), - nostrconnect_url: None, - }; + let handler_spec = ApplicationHandlerSpec::new(super::service_presence_kinds()) + .with_identifier("radrootsd") + .with_metadata(settings.metadata); let error = super::bootstrap_presence(&client, &identity, &profile, &handler_spec) .await diff --git a/src/core/nip46/session.rs b/src/core/nip46/session.rs @@ -7,8 +7,8 @@ use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; +use crate::host_nostr::{DaemonNostrClient, Keys, PublicKey}; use nostr::nips::nip46::NostrConnectRequest; -use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrKeys, RadrootsNostrPublicKey}; #[derive(Clone)] pub struct Nip46SessionStore { @@ -19,7 +19,7 @@ pub struct Nip46SessionStore { #[derive(Clone)] pub struct PendingNostrRequest { pub request_id: String, - pub client_pubkey: RadrootsNostrPublicKey, + pub client_pubkey: PublicKey, pub request: NostrConnectRequest, } @@ -65,11 +65,11 @@ pub struct Nip46SessionAuthority { #[derive(Clone)] pub struct Nip46Session { pub id: String, - pub client: RadrootsNostrClient, - pub client_keys: RadrootsNostrKeys, - pub client_pubkey: RadrootsNostrPublicKey, - pub remote_signer_pubkey: RadrootsNostrPublicKey, - pub user_pubkey: Option<RadrootsNostrPublicKey>, + pub(crate) client: DaemonNostrClient, + pub client_keys: Keys, + pub client_pubkey: PublicKey, + pub remote_signer_pubkey: PublicKey, + pub user_pubkey: Option<PublicKey>, pub relays: Vec<String>, pub perms: Vec<String>, pub name: Option<String>, @@ -114,7 +114,7 @@ impl Nip46SessionStore { sessions.remove(session_id).is_some() } - pub async fn set_user_pubkey(&self, session_id: &str, pubkey: RadrootsNostrPublicKey) -> bool { + pub async fn set_user_pubkey(&self, session_id: &str, pubkey: PublicKey) -> bool { let mut sessions = self.inner.lock().await; match sessions.get_mut(session_id) { Some(session) => { @@ -322,8 +322,8 @@ mod tests { use super::*; fn build_session(id: &str, expires_at: Option<Instant>) -> Nip46Session { - let keys = RadrootsNostrKeys::generate(); - let client = RadrootsNostrClient::new(keys.clone()); + let keys = Keys::generate(); + let client = DaemonNostrClient::with_keys(keys.clone()); let pubkey = keys.public_key(); Nip46Session { id: id.to_string(), @@ -372,11 +372,11 @@ mod tests { #[test] fn public_view_marks_outbound_remote_signer_sessions() { - let client_keys = RadrootsNostrKeys::generate(); - let remote_signer_keys = RadrootsNostrKeys::generate(); + let client_keys = Keys::generate(); + let remote_signer_keys = Keys::generate(); let session = Nip46Session { id: "outbound".to_string(), - client: RadrootsNostrClient::new(client_keys.clone()), + client: DaemonNostrClient::with_keys(client_keys.clone()), client_keys: client_keys.clone(), client_pubkey: client_keys.public_key(), remote_signer_pubkey: remote_signer_keys.public_key(), @@ -410,12 +410,12 @@ mod tests { #[test] fn public_view_keeps_remote_signer_and_user_pubkeys_distinct() { - let client_keys = RadrootsNostrKeys::generate(); - let remote_signer_keys = RadrootsNostrKeys::generate(); - let user_keys = RadrootsNostrKeys::generate(); + let client_keys = Keys::generate(); + let remote_signer_keys = Keys::generate(); + let user_keys = Keys::generate(); let session = Nip46Session { id: "hydrated-outbound".to_string(), - client: RadrootsNostrClient::new(client_keys.clone()), + client: DaemonNostrClient::with_keys(client_keys.clone()), client_keys: client_keys.clone(), client_pubkey: client_keys.public_key(), remote_signer_pubkey: remote_signer_keys.public_key(), @@ -538,7 +538,7 @@ mod tests { #[tokio::test] async fn session_store_set_user_pubkey_handles_missing_and_expired() { let store = Nip46SessionStore::new(); - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); assert!(!store.set_user_pubkey("missing", keys.public_key()).await); let session = build_session( @@ -560,7 +560,7 @@ mod tests { "active-user", Some(Instant::now() + Duration::from_secs(30)), ); - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let pubkey = keys.public_key(); store.insert(session).await; assert!(store.set_user_pubkey("active-user", pubkey).await); @@ -572,7 +572,7 @@ mod tests { async fn session_store_require_auth_sets_flags_and_clears_pending() { let store = Nip46SessionStore::new(); let mut session = build_session("auth", Some(Instant::now() + Duration::from_secs(30))); - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); session.pending_request = Some(PendingNostrRequest { request_id: "req-1".to_string(), client_pubkey: keys.public_key(), @@ -615,7 +615,7 @@ mod tests { let store = Nip46SessionStore::new(); let mut session = build_session("authorize", Some(Instant::now() + Duration::from_secs(30))); - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); session.pending_request = Some(PendingNostrRequest { request_id: "req-2".to_string(), client_pubkey: keys.public_key(), @@ -646,7 +646,7 @@ mod tests { #[tokio::test] async fn session_store_set_pending_request_handles_missing_and_expired() { let store = Nip46SessionStore::new(); - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let pending = PendingNostrRequest { request_id: "req-3".to_string(), client_pubkey: keys.public_key(), @@ -671,7 +671,7 @@ mod tests { Some(Instant::now() + Duration::from_secs(30)), )) .await; - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let pending = PendingNostrRequest { request_id: "req-active".to_string(), client_pubkey: keys.public_key(), diff --git a/src/core/state.rs b/src/core/state.rs @@ -1,18 +1,16 @@ +use crate::app::identity_storage::DaemonIdentity; +use crate::host_nostr::{DaemonNostrClient, Keys, Metadata, PublicKey}; use anyhow::Result; -use radroots_identity::RadrootsIdentity; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrKeys, RadrootsNostrMetadata, RadrootsNostrPublicKey, -}; use crate::app::config::{Nip46Config, TransportPublishConfig}; use crate::core::transport_publish::TransportPublish; #[derive(Clone)] pub struct Radrootsd { - pub client: RadrootsNostrClient, - pub keys: RadrootsNostrKeys, - pub pubkey: RadrootsNostrPublicKey, - pub metadata: RadrootsNostrMetadata, + pub(crate) client: DaemonNostrClient, + pub keys: Keys, + pub pubkey: PublicKey, + pub metadata: Metadata, pub info: serde_json::Value, pub transport_publish: TransportPublish, pub(crate) nip46_sessions: crate::core::nip46::session::Nip46SessionStore, @@ -20,15 +18,15 @@ pub struct Radrootsd { } impl Radrootsd { - pub fn new( - identity: RadrootsIdentity, - metadata: RadrootsNostrMetadata, + pub(crate) fn new( + identity: DaemonIdentity, + metadata: Metadata, transport_publish_config: TransportPublishConfig, nip46_config: Nip46Config, ) -> Result<Self> { - let keys: RadrootsNostrKeys = identity.keys().clone(); + let keys: Keys = identity.keys().clone(); let pubkey = keys.public_key(); - let client = RadrootsNostrClient::new(keys.clone()); + let client = DaemonNostrClient::with_keys(keys.clone()); let info = serde_json::json!({ "version": env!("CARGO_PKG_VERSION"), "build": option_env!("GIT_HASH").unwrap_or("unknown"), @@ -56,13 +54,13 @@ impl Radrootsd { mod tests { use super::Radrootsd; use crate::app::config::{Nip46Config, TransportPublishConfig}; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::RadrootsNostrMetadata; + use crate::app::identity_storage::DaemonIdentity; + use crate::host_nostr::Metadata; #[test] fn new_sets_core_fields() { - let identity = RadrootsIdentity::generate(); - let metadata: RadrootsNostrMetadata = + let identity = DaemonIdentity::generate(); + let metadata: Metadata = serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata"); let transport_publish_cfg = TransportPublishConfig::default(); let cfg = Nip46Config::default(); diff --git a/src/core/transport_publish.rs b/src/core/transport_publish.rs @@ -8,30 +8,29 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use radroots_event::draft::{ - RadrootsSignatureVerificationError, RadrootsSignedEvent, RadrootsSignedEventError, +use crate::host_nostr::{DaemonNostrClient, Filter, Kind, PublicKey}; +use crate::transport::relay_publish::{ + LiveRelayPublishAdapter as DaemonNostrClientPublishAdapter, + RelayOutcome as RadrootsRelayOutcome, RelayOutcomeKind as RadrootsRelayOutcomeKind, + RelayPublishAdapter as RadrootsRelayPublishAdapter, + RelayPublishReceipt as RadrootsRelayPublishRelayReceipt, + RelayPublishRequest as RadrootsRelayPublishRequest, RelayTargetSet as RadrootsRelayTargetSet, + RelayUrl as RadrootsRelayUrl, RelayUrlPolicy as RadrootsRelayUrlPolicy, }; -use radroots_event::wire::{RadrootsEventWireError, RadrootsNip01EventWire}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, +use radroots_event::draft::{SignedEvent, SignedEventError}; +use radroots_event::wire::{EventWireError, Nip01EventWire}; +use radroots_protocol::radrootsd::transport_publish::v5::{ + DeliveryPolicy, EventRequest, EventResponse, Job, JobStatus, NostrTargetSourcePolicy, + OutcomeKind, RETICULUM_ENDPOINT_URI as RADROOTS_RETICULUM_ENDPOINT_URI, + RETICULUM_UNAVAILABLE_MESSAGE as RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, Target, + TargetFingerprint as ProtocolTargetFingerprint, TargetOutcome, TargetPolicy, TargetPolicyName, + TargetSource, }; use radroots_transport::{ - RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportKind, - RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionClass, + RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionClass, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetFingerprint, RadrootsTransportTargetLabel, }; -use radroots_transport_nostr::{ - RadrootsNostrClientPublishAdapter, RadrootsRelayOutcome, RadrootsRelayOutcomeKind, - RadrootsRelayPublishAdapter, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, - RadrootsRelayTargetSet, RadrootsRelayTransportError, RadrootsRelayUrl, RadrootsRelayUrlPolicy, -}; -use radroots_transport_publish_protocol::{ - NostrPublishTargetSourcePolicy, TransportPublishDeliveryPolicy, TransportPublishEventRequest, - TransportPublishEventResponse, TransportPublishJobStatus, TransportPublishJobView, - TransportPublishOutcomeKind, TransportPublishTarget, TransportPublishTargetOutcome, - TransportPublishTargetPolicy, TransportPublishTargetPolicyName, TransportPublishTargetSource, -}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection, SqliteRow}; @@ -144,13 +143,11 @@ pub enum TransportPublishError { #[error("invalid signed Nostr event: {0}")] InvalidSignedEvent(String), #[error("signed event wire error: {0}")] - EventWire(#[from] RadrootsEventWireError), + EventWire(#[from] EventWireError), #[error("signed event conversion error: {0}")] - SignedEvent(#[from] RadrootsSignedEventError), - #[error("signed event signature verification failed: {0}")] - SignedEventSignature(#[from] RadrootsSignatureVerificationError), + SignedEvent(#[from] SignedEventError), #[error("transport publish relay error: {0}")] - Relay(#[from] RadrootsRelayTransportError), + Relay(String), #[error("transport publish transport error: {0}")] Transport(String), #[error("transport publish schema incompatible for table `{table}`: {detail}")] @@ -200,7 +197,11 @@ impl TransportPublish { }) } - pub fn with_publisher(mut self, publisher: Arc<dyn RadrootsRelayPublishAdapter>) -> Self { + #[cfg(test)] + pub(crate) fn with_publisher( + mut self, + publisher: Arc<dyn RadrootsRelayPublishAdapter>, + ) -> Self { self.publisher = Some(publisher); self } @@ -230,8 +231,8 @@ impl TransportPublish { pub async fn publish_event( &self, principal: &PublishPrincipal, - request: TransportPublishEventRequest, - ) -> Result<TransportPublishEventResponse, TransportPublishError> { + request: EventRequest, + ) -> Result<EventResponse, TransportPublishError> { request .validate(self.config.max_targets_per_request) .map_err(|error| { @@ -254,8 +255,9 @@ impl TransportPublish { &request, effective_timeout_ms, )?; + let signed_pubkey = signed_event.pubkey().to_hex(); let resolution = self - .resolve_targets_for_request(signed_event.pubkey_str(), &request) + .resolve_targets_for_request(signed_pubkey.as_str(), &request) .await?; validate_delivery_policy_for_resolution(&request.delivery_policy, &resolution)?; let target_snapshots = target_snapshots_from_resolution(&resolution); @@ -280,7 +282,7 @@ impl TransportPublish { resolution, ) .await?; - Ok(TransportPublishEventResponse { + Ok(EventResponse { deduplicated: false, job: completed, }) @@ -289,30 +291,30 @@ impl TransportPublish { pub async fn resolve_targets_for_request( &self, pubkey: &str, - request: &TransportPublishEventRequest, + request: &EventRequest, ) -> Result<PublishRelayResolution, TransportPublishError> { match &request.target_policy { - TransportPublishTargetPolicy::ExplicitTargets { targets } => { + TargetPolicy::ExplicitTargets { targets } => { self.resolve_explicit_targets(targets).await } - TransportPublishTargetPolicy::Nostr { + TargetPolicy::Nostr { source_policy, relay_urls, } => match source_policy { - NostrPublishTargetSourcePolicy::ExplicitOnly => { + NostrTargetSourcePolicy::ExplicitOnly => { self.resolve_request_relays(relay_urls).await } - NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault => { + NostrTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault => { if !relay_urls.is_empty() { self.resolve_request_relays(relay_urls).await } else { self.resolve_author_or_default_relays(pubkey).await } } - NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault => { + NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault => { self.resolve_author_or_default_relays(pubkey).await } - NostrPublishTargetSourcePolicy::DaemonDefaultOnly => { + NostrTargetSourcePolicy::DaemonDefaultOnly => { self.resolve_daemon_default_relays().await } }, @@ -321,7 +323,7 @@ impl TransportPublish { async fn resolve_explicit_targets( &self, - targets: &[TransportPublishTarget], + targets: &[Target], ) -> Result<PublishRelayResolution, TransportPublishError> { let mut resolved = Vec::new(); let mut outcomes = Vec::new(); @@ -352,8 +354,8 @@ impl TransportPublish { async fn resolve_request_target( &self, targets: &mut Vec<ResolvedPublishRelay>, - outcomes: &mut Vec<TransportPublishTargetOutcome>, - target: &TransportPublishTarget, + outcomes: &mut Vec<TargetOutcome>, + target: &Target, ) { match RadrootsRelayUrl::parse(target.endpoint_uri.as_str(), relay_url_policy(&self.config)) { @@ -362,19 +364,19 @@ impl TransportPublish { targets, outcomes, url, - TransportPublishTargetSource::Request, + TargetSource::Request, PublishTargetMetadata::from_target(target), ) .await; } - Err(error) => outcomes.push(TransportPublishTargetOutcome { + Err(error) => outcomes.push(TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), - endpoint_uri: target.endpoint_uri.trim().to_owned(), + endpoint_uri: target.endpoint_uri.clone(), target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), - source: TransportPublishTargetSource::Request, + source: TargetSource::Request, attempted: false, - outcome_kind: TransportPublishOutcomeKind::TargetRejected, + outcome_kind: OutcomeKind::TargetRejected, message: Some(error.to_string()), latency_ms: None, }), @@ -407,19 +409,19 @@ impl TransportPublish { &mut targets, &mut outcomes, url, - TransportPublishTargetSource::Request, + TargetSource::Request, PublishTargetMetadata::default(), ) .await; } - Err(error) => outcomes.push(TransportPublishTargetOutcome { + Err(error) => outcomes.push(TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), - endpoint_uri: relay.trim().to_owned(), + endpoint_uri: relay.clone(), target_scope: None, target_label: None, - source: TransportPublishTargetSource::Request, + source: TargetSource::Request, attempted: false, - outcome_kind: TransportPublishOutcomeKind::TargetRejected, + outcome_kind: OutcomeKind::TargetRejected, message: Some(error.to_string()), latency_ms: None, }), @@ -443,7 +445,7 @@ impl TransportPublish { let mut discovery_targets = self .resolve_config_relays( &self.config.nostr.author_relay_discovery_relays, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, ) .await?; if discovery_targets.targets.is_empty() { @@ -484,22 +486,12 @@ impl TransportPublish { &mut targets, &mut outcomes, url, - TransportPublishTargetSource::NostrAuthorWrite, + TargetSource::NostrAuthorWrite, PublishTargetMetadata::default(), ) .await; } - Err(error) => outcomes.push(TransportPublishTargetOutcome { - transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), - endpoint_uri: relay.trim().to_owned(), - target_scope: None, - target_label: None, - source: TransportPublishTargetSource::NostrAuthorWrite, - attempted: false, - outcome_kind: TransportPublishOutcomeKind::TargetRejected, - message: Some(error.to_string()), - latency_ms: None, - }), + Err(_error) => {} } } Ok(PublishRelayResolution { targets, outcomes }) @@ -510,7 +502,7 @@ impl TransportPublish { ) -> Result<PublishRelayResolution, TransportPublishError> { self.resolve_config_relays( &self.config.nostr.daemon_default_relays, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, ) .await } @@ -518,7 +510,7 @@ impl TransportPublish { async fn resolve_config_relays( &self, relays: &[String], - source: TransportPublishTargetSource, + source: TargetSource, ) -> Result<PublishRelayResolution, TransportPublishError> { let mut targets = Vec::new(); let mut outcomes = Vec::new(); @@ -534,17 +526,7 @@ impl TransportPublish { ) .await; } - Err(error) => outcomes.push(TransportPublishTargetOutcome { - transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), - endpoint_uri: relay.trim().to_owned(), - target_scope: None, - target_label: None, - source, - attempted: false, - outcome_kind: TransportPublishOutcomeKind::TargetRejected, - message: Some(error.to_string()), - latency_ms: None, - }), + Err(_error) => {} } } Ok(PublishRelayResolution { targets, outcomes }) @@ -553,9 +535,9 @@ impl TransportPublish { async fn push_checked_relay_target( &self, targets: &mut Vec<ResolvedPublishRelay>, - outcomes: &mut Vec<TransportPublishTargetOutcome>, + outcomes: &mut Vec<TargetOutcome>, url: RadrootsRelayUrl, - source: TransportPublishTargetSource, + source: TargetSource, metadata: PublishTargetMetadata, ) { if relay_url_policy(&self.config) == RadrootsRelayUrlPolicy::Localhost { @@ -573,14 +555,14 @@ impl TransportPublish { } Ok(addresses) => match url.validate_public_resolved_ip_addrs(addresses) { Ok(()) => push_resolved_relay(targets, url, source, metadata), - Err(error) => outcomes.push(TransportPublishTargetOutcome { + Err(error) => outcomes.push(TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), endpoint_uri: url.as_str().to_owned(), target_scope: metadata.target_scope, target_label: metadata.target_label, source, attempted: false, - outcome_kind: TransportPublishOutcomeKind::TargetRejected, + outcome_kind: OutcomeKind::TargetRejected, message: Some(error.to_string()), latency_ms: None, }), @@ -597,15 +579,15 @@ impl TransportPublish { async fn complete_job_execution( &self, job_id: &str, - signed_event: RadrootsSignedEvent, - delivery_policy: TransportPublishDeliveryPolicy, + signed_event: SignedEvent, + delivery_policy: DeliveryPolicy, timeout_ms: u64, resolution: PublishRelayResolution, - ) -> Result<TransportPublishJobView, TransportPublishError> { + ) -> Result<Job, TransportPublishError> { let target_count = resolution.target_count(); if resolution.targets.is_empty() { let status = if resolution.outcomes.is_empty() { - TransportPublishJobStatus::Rejected + JobStatus::Rejected } else { delivery_status(&delivery_policy, target_count, &resolution.outcomes) }; @@ -622,7 +604,7 @@ impl TransportPublish { if required_target_count > target_count { self.store.complete_publish_job( job_id, - TransportPublishJobStatus::Rejected, + JobStatus::Rejected, resolution.outcomes, Some("delivery_quorum_exceeds_target_count".to_owned()), )?; @@ -634,7 +616,8 @@ impl TransportPublish { .iter() .map(|target| target.url.clone()) .collect(), - )?; + ) + .map_err(|error| TransportPublishError::Relay(error.to_string()))?; let satisfaction_policy = satisfaction_policy_from_delivery_policy( &delivery_policy, target_count, @@ -674,13 +657,13 @@ impl TransportPublish { return publisher .publish(request) .await - .map_err(TransportPublishError::Relay); + .map_err(|error| TransportPublishError::Relay(error.to_string())); } - let adapter = RadrootsNostrClientPublishAdapter::new(RadrootsNostrClient::new_signerless()); + let adapter = DaemonNostrClientPublishAdapter::new(DaemonNostrClient::signerless()); adapter .publish(request) .await - .map_err(TransportPublishError::Relay) + .map_err(|error| TransportPublishError::Relay(error.to_string())) } } @@ -725,9 +708,9 @@ pub struct PublishPrincipalInit { pub token_hash: String, pub allowed_pubkeys: Vec<String>, pub allowed_kinds: Vec<u32>, - pub allowed_target_policies: Vec<TransportPublishTargetPolicyName>, + pub allowed_target_policies: Vec<TargetPolicyName>, pub allowed_explicit_transport_kinds: Vec<String>, - pub allowed_nostr_source_policies: Vec<NostrPublishTargetSourcePolicy>, + pub allowed_nostr_source_policies: Vec<NostrTargetSourcePolicy>, pub allow_request_targets: bool, pub job_visibility: PublishJobVisibility, pub expires_at_unix: Option<i64>, @@ -739,9 +722,9 @@ pub struct PublishPrincipal { pub label: String, pub allowed_pubkeys: Vec<String>, pub allowed_kinds: Vec<u32>, - pub allowed_target_policies: Vec<TransportPublishTargetPolicyName>, + pub allowed_target_policies: Vec<TargetPolicyName>, pub allowed_explicit_transport_kinds: Vec<String>, - pub allowed_nostr_source_policies: Vec<NostrPublishTargetSourcePolicy>, + pub allowed_nostr_source_policies: Vec<NostrTargetSourcePolicy>, pub allow_request_targets: bool, pub job_visibility: PublishJobVisibility, pub expires_at_unix: Option<i64>, @@ -750,15 +733,15 @@ pub struct PublishPrincipal { impl PublishPrincipal { pub fn allows_event( &self, - signed_event: &RadrootsSignedEvent, - request: &TransportPublishEventRequest, + signed_event: &SignedEvent, + request: &EventRequest, ) -> Result<(), TransportPublishError> { - let pubkey = signed_event.pubkey_str(); - ensure_lower_hex("pubkey", pubkey, 64)?; + let pubkey = signed_event.pubkey().to_hex(); + ensure_lower_hex("pubkey", pubkey.as_str(), 64)?; if !self .allowed_pubkeys .iter() - .any(|allowed_pubkey| allowed_pubkey == pubkey) + .any(|allowed_pubkey| allowed_pubkey == pubkey.as_str()) { return Err(TransportPublishError::InvalidScope( "principal is not allowed to publish for event pubkey".to_owned(), @@ -770,10 +753,10 @@ impl PublishPrincipal { )); } match &request.target_policy { - TransportPublishTargetPolicy::ExplicitTargets { targets } => { + TargetPolicy::ExplicitTargets { targets } => { if !self .allowed_target_policies - .contains(&TransportPublishTargetPolicyName::ExplicitTargets) + .contains(&TargetPolicyName::ExplicitTargets) { return Err(TransportPublishError::InvalidScope( "principal is not allowed to use explicit transport targets".to_owned(), @@ -804,13 +787,13 @@ impl PublishPrincipal { } } } - TransportPublishTargetPolicy::Nostr { + TargetPolicy::Nostr { source_policy, relay_urls, } => { if !self .allowed_target_policies - .contains(&TransportPublishTargetPolicyName::Nostr) + .contains(&TargetPolicyName::Nostr) { return Err(TransportPublishError::InvalidScope( "principal is not allowed to use Nostr target policy".to_owned(), @@ -841,10 +824,10 @@ pub struct PublishJobInsert { pub principal_id: String, pub idempotency_key: Option<String>, pub event: PublishEventMetadata, - pub request: TransportPublishEventRequest, + pub request: EventRequest, pub request_fingerprint: String, pub effective_target_count: usize, - pub target_snapshots: Vec<TransportPublishTargetOutcome>, + pub target_snapshots: Vec<TargetOutcome>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -855,10 +838,10 @@ pub struct PublishEventMetadata { } impl PublishEventMetadata { - fn from_signed_event(signed_event: &RadrootsSignedEvent) -> Self { + fn from_signed_event(signed_event: &SignedEvent) -> Self { Self { event_id: signed_event.id_str().to_owned(), - pubkey: signed_event.pubkey_str().to_owned(), + pubkey: signed_event.pubkey().to_hex(), kind: signed_event.kind(), } } @@ -866,8 +849,8 @@ impl PublishEventMetadata { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedPublishRelay { - pub url: RadrootsRelayUrl, - pub source: TransportPublishTargetSource, + pub(crate) url: RadrootsRelayUrl, + pub source: TargetSource, target_scope: Option<String>, target_label: Option<String>, } @@ -875,7 +858,7 @@ pub struct ResolvedPublishRelay { #[derive(Debug, Clone, PartialEq, Eq)] pub struct PublishRelayResolution { pub targets: Vec<ResolvedPublishRelay>, - pub outcomes: Vec<TransportPublishTargetOutcome>, + pub outcomes: Vec<TargetOutcome>, } impl PublishRelayResolution { @@ -914,7 +897,7 @@ impl ResolvedPublishRelay { let target = RadrootsTransportTarget::nostr_relay_with_metadata(self.url.as_str(), scope, label) .map_err(|error| TransportPublishError::Transport(error.to_string()))?; - Ok(target.fingerprint) + Ok(target.fingerprint().clone()) } } @@ -925,7 +908,7 @@ struct PublishTargetMetadata { } impl PublishTargetMetadata { - fn from_target(target: &TransportPublishTarget) -> Self { + fn from_target(target: &Target) -> Self { Self { target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), @@ -976,18 +959,18 @@ impl PublishAuthorRelayDiscovery for NostrPublishAuthorRelayDiscovery { connect_timeout_secs: u64, ) -> PublishAuthorRelayDiscoveryFuture<'a> { Box::pin(async move { - let Ok(public_key) = RadrootsNostrPublicKey::from_hex(pubkey) else { + let Ok(public_key) = PublicKey::from_hex(pubkey) else { return Ok(Vec::new()); }; - let client = RadrootsNostrClient::new_signerless(); + let client = DaemonNostrClient::signerless(); for target in discovery_targets { if client.add_read_relay(target.url.as_str()).await.is_err() { return Ok(Vec::new()); } } - let filter = RadrootsNostrFilter::new() + let filter = Filter::new() .author(public_key) - .kind(RadrootsNostrKind::Custom(10_002)) + .kind(Kind::Custom(10_002)) .limit(10); let timeout = Duration::from_secs(connect_timeout_secs); let Ok(events) = client.fetch_events(filter, timeout).await else { @@ -1178,7 +1161,7 @@ impl TransportPublishStore { pub fn record_publish_job( &self, insert: PublishJobInsert, - ) -> Result<TransportPublishEventResponse, TransportPublishError> { + ) -> Result<EventResponse, TransportPublishError> { if insert.effective_target_count != insert.target_snapshots.len() { return Err(TransportPublishError::InvalidScope( "publish job target snapshot count must match effective target count".to_owned(), @@ -1193,7 +1176,7 @@ impl TransportPublishStore { idempotency_key.to_owned(), )); } - return Ok(TransportPublishEventResponse { + return Ok(EventResponse { deduplicated: true, job: existing.view, }); @@ -1241,9 +1224,7 @@ impl TransportPublishStore { .bind(insert.principal_id.as_str()) .bind(insert.idempotency_key.as_deref()) .bind(insert.request_fingerprint.as_str()) - .bind(serde_json::to_string( - &TransportPublishJobStatus::Publishing, - )?) + .bind(serde_json::to_string(&JobStatus::Publishing)?) .bind(insert.event.event_id.as_str()) .bind(insert.event.pubkey.as_str()) .bind(i64::from(insert.event.kind)) @@ -1284,7 +1265,7 @@ impl TransportPublishStore { } drop(connection); let job = self.job_by_id(job_id.as_str())?; - Ok(TransportPublishEventResponse { + Ok(EventResponse { deduplicated: false, job, }) @@ -1294,7 +1275,7 @@ impl TransportPublishStore { &self, job_id: &str, principal: &PublishPrincipal, - ) -> Result<Option<TransportPublishJobView>, TransportPublishError> { + ) -> Result<Option<Job>, TransportPublishError> { let mut connection = self .inner .lock() @@ -1322,7 +1303,7 @@ impl TransportPublishStore { &self, principal: &PublishPrincipal, limit: usize, - ) -> Result<Vec<TransportPublishJobView>, TransportPublishError> { + ) -> Result<Vec<Job>, TransportPublishError> { let limit = i64::try_from(limit.clamp(1, 200)).unwrap_or(200); let mut connection = self .inner @@ -1389,10 +1370,7 @@ impl TransportPublishStore { Ok(Some(job)) } - pub fn job_by_id( - &self, - job_id: &str, - ) -> Result<TransportPublishJobView, TransportPublishError> { + pub fn job_by_id(&self, job_id: &str) -> Result<Job, TransportPublishError> { let mut connection = self .inner .lock() @@ -1418,8 +1396,8 @@ impl TransportPublishStore { pub fn complete_publish_job( &self, job_id: &str, - status: TransportPublishJobStatus, - outcomes: Vec<TransportPublishTargetOutcome>, + status: JobStatus, + outcomes: Vec<TargetOutcome>, last_error: Option<String>, ) -> Result<(), TransportPublishError> { let now = current_unix_millis(); @@ -1500,10 +1478,7 @@ impl TransportPublishStore { Ok(()) } - fn target_outcomes( - &self, - job_id: &str, - ) -> Result<Vec<TransportPublishTargetOutcome>, TransportPublishError> { + fn target_outcomes(&self, job_id: &str) -> Result<Vec<TargetOutcome>, TransportPublishError> { let mut connection = self .inner .lock() @@ -1543,7 +1518,7 @@ impl TransportPublishStore { struct PublishJobRow { principal_id: String, request_fingerprint: String, - view: TransportPublishJobView, + view: Job, } enum TransportPublishSchemaState { @@ -2165,7 +2140,7 @@ fn recover_interrupted_publish_jobs( connection: &mut SqliteConnection, ) -> Result<(), TransportPublishError> { let now = current_unix_millis(); - let publishing = serde_json::to_string(&TransportPublishJobStatus::Publishing)?; + let publishing = serde_json::to_string(&JobStatus::Publishing)?; let sql = job_select_sql("WHERE status = ?1"); let rows = block_on_sqlite( sqlx::query(sqlx::AssertSqlSafe(sql.as_str())) @@ -2193,7 +2168,7 @@ fn recover_interrupted_publish_jobs( "#, ) .bind(job_id.as_str()) - .bind(serde_json::to_string(&TransportPublishJobStatus::Rejected)?) + .bind(serde_json::to_string(&JobStatus::Rejected)?) .bind(now) .bind(now) .bind("publish_attempt_interrupted_missing_target_snapshot") @@ -2204,7 +2179,7 @@ fn recover_interrupted_publish_jobs( } let status = delivery_status(&row.view.delivery_policy, snapshots.len(), &snapshots); let effective_target_count = storage_count_i64(snapshots.len(), "effective_target_count")?; - let last_error = if status == TransportPublishJobStatus::DeliveryUnsatisfiedRetryable { + let last_error = if status == JobStatus::DeliveryUnsatisfiedRetryable { Some("publish_attempt_interrupted".to_owned()) } else { last_error_for_status(status).map(str::to_owned) @@ -2237,7 +2212,7 @@ fn recover_interrupted_publish_jobs( fn insert_target_snapshots( connection: &mut SqliteConnection, job_id: &str, - outcomes: &[TransportPublishTargetOutcome], + outcomes: &[TargetOutcome], now: i64, ) -> Result<(), TransportPublishError> { for (target_index, outcome) in outcomes.iter().enumerate() { @@ -2286,7 +2261,7 @@ fn insert_target_snapshots( fn replace_target_outcomes( connection: &mut SqliteConnection, job_id: &str, - outcomes: &[TransportPublishTargetOutcome], + outcomes: &[TargetOutcome], now: i64, ) -> Result<(), TransportPublishError> { block_on_sqlite( @@ -2338,7 +2313,7 @@ fn replace_target_outcomes( fn target_snapshot_outcomes( connection: &mut SqliteConnection, job_id: &str, -) -> Result<Vec<TransportPublishTargetOutcome>, TransportPublishError> { +) -> Result<Vec<TargetOutcome>, TransportPublishError> { let rows = block_on_sqlite( sqlx::query( r#" @@ -2402,14 +2377,13 @@ fn principal_from_row(row: &SqliteRow) -> Result<PublishPrincipal, TransportPubl } fn job_from_row(row: &SqliteRow) -> Result<PublishJobRow, TransportPublishError> { - let status: TransportPublishJobStatus = json_text(row, 3, "status")?; - let target_policy: TransportPublishTargetPolicy = json_text(row, 7, "target_policy_json")?; - let delivery_policy: TransportPublishDeliveryPolicy = - json_text(row, 8, "delivery_policy_json")?; + let status: JobStatus = json_text(row, 3, "status")?; + let target_policy: TargetPolicy = json_text(row, 7, "target_policy_json")?; + let delivery_policy: DeliveryPolicy = json_text(row, 8, "delivery_policy_json")?; Ok(PublishJobRow { principal_id: row.try_get(1)?, request_fingerprint: row.try_get(2)?, - view: TransportPublishJobView { + view: Job { job_id: row.try_get(0)?, status, terminal: false, @@ -2431,12 +2405,10 @@ fn job_from_row(row: &SqliteRow) -> Result<PublishJobRow, TransportPublishError> }) } -fn target_outcome_from_row( - row: &SqliteRow, -) -> Result<TransportPublishTargetOutcome, TransportPublishError> { - let source: TransportPublishTargetSource = json_text(row, 4, "source")?; - let outcome_kind: TransportPublishOutcomeKind = json_text(row, 6, "outcome_kind")?; - Ok(TransportPublishTargetOutcome { +fn target_outcome_from_row(row: &SqliteRow) -> Result<TargetOutcome, TransportPublishError> { + let source: TargetSource = json_text(row, 4, "source")?; + let outcome_kind: OutcomeKind = json_text(row, 6, "outcome_kind")?; + Ok(TargetOutcome { transport_kind: row.try_get(0)?, endpoint_uri: row.try_get(1)?, target_scope: storage_target_scope_to_protocol(row.try_get::<String, _>(2)?), @@ -2449,7 +2421,7 @@ fn target_outcome_from_row( }) } -fn finalize_job_view(view: &mut TransportPublishJobView) { +fn finalize_job_view(view: &mut Job) { view.acknowledged_count = view .targets .iter() @@ -2467,13 +2439,13 @@ fn finalize_job_view(view: &mut TransportPublishJobView) { .count(); view.terminal = matches!( view.status, - TransportPublishJobStatus::DeliverySatisfied - | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal - | TransportPublishJobStatus::DeliveryDeferred - | TransportPublishJobStatus::DeliveryDeferredUntilImplemented - | TransportPublishJobStatus::Rejected + JobStatus::DeliverySatisfied + | JobStatus::DeliveryUnsatisfiedTerminal + | JobStatus::DeliveryDeferred + | JobStatus::DeliveryDeferredUntilImplemented + | JobStatus::Rejected ); - view.delivery_satisfied = view.status == TransportPublishJobStatus::DeliverySatisfied; + view.delivery_satisfied = view.status == JobStatus::DeliverySatisfied; } fn validate_principal_init(input: &PublishPrincipalInit) -> Result<(), TransportPublishError> { @@ -2507,7 +2479,7 @@ fn validate_principal_init(input: &PublishPrincipalInit) -> Result<(), Transport } let allows_explicit_targets = input .allowed_target_policies - .contains(&TransportPublishTargetPolicyName::ExplicitTargets); + .contains(&TargetPolicyName::ExplicitTargets); if allows_explicit_targets && input.allowed_explicit_transport_kinds.is_empty() { return Err(TransportPublishError::InvalidScope( "principal must include at least one allowed explicit transport kind".to_owned(), @@ -2530,7 +2502,7 @@ fn validate_principal_init(input: &PublishPrincipalInit) -> Result<(), Transport } if input .allowed_target_policies - .contains(&TransportPublishTargetPolicyName::Nostr) + .contains(&TargetPolicyName::Nostr) && input.allowed_nostr_source_policies.is_empty() { return Err(TransportPublishError::InvalidScope( @@ -2562,28 +2534,26 @@ fn hex_lower(bytes: &[u8]) -> String { pub fn parse_nostr_source_policy( value: &str, -) -> Result<NostrPublishTargetSourcePolicy, TransportPublishError> { +) -> Result<NostrTargetSourcePolicy, TransportPublishError> { match value { - "explicit_only" => Ok(NostrPublishTargetSourcePolicy::ExplicitOnly), + "explicit_only" => Ok(NostrTargetSourcePolicy::ExplicitOnly), "request_then_author_write_then_daemon_default" => { - Ok(NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault) + Ok(NostrTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault) } "author_write_then_daemon_default" => { - Ok(NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault) + Ok(NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault) } - "daemon_default_only" => Ok(NostrPublishTargetSourcePolicy::DaemonDefaultOnly), + "daemon_default_only" => Ok(NostrTargetSourcePolicy::DaemonDefaultOnly), other => Err(TransportPublishError::InvalidScope(format!( "unknown Nostr source policy `{other}`" ))), } } -pub fn parse_target_policy( - value: &str, -) -> Result<TransportPublishTargetPolicyName, TransportPublishError> { +pub fn parse_target_policy(value: &str) -> Result<TargetPolicyName, TransportPublishError> { match value { - "explicit_targets" => Ok(TransportPublishTargetPolicyName::ExplicitTargets), - "nostr" => Ok(TransportPublishTargetPolicyName::Nostr), + "explicit_targets" => Ok(TargetPolicyName::ExplicitTargets), + "nostr" => Ok(TargetPolicyName::Nostr), other => Err(TransportPublishError::InvalidScope(format!( "unknown target policy `{other}`" ))), @@ -2596,29 +2566,40 @@ pub fn parse_explicit_transport_kind(value: &str) -> Result<String, TransportPub "unknown explicit transport kind `{value}`: {error}" )) })?; - Ok(kind.canonical_label()) + let canonical = kind.canonical_label(); + if !matches!(canonical.as_str(), "local" | "nostr" | "reticulum") { + return Err(TransportPublishError::InvalidScope(format!( + "unknown explicit transport kind `{value}`" + ))); + } + Ok(canonical) } -fn signed_event_from_raw_json( - raw_json: &str, -) -> Result<RadrootsSignedEvent, TransportPublishError> { - let wire = RadrootsNip01EventWire::parse_json(raw_json)?; - let signed_event = RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.to_owned())?; - Ok(signed_event.verify_signature()?.into_signed_event()) +fn signed_event_from_raw_json(raw_json: &str) -> Result<SignedEvent, TransportPublishError> { + let wire = Nip01EventWire::parse_json(raw_json)?; + let signed_event = SignedEvent::from_wire_verified_id(wire, raw_json.to_owned())?; + if radroots_nostr::event::verify(signed_event.envelope()) + != radroots_nostr::event::Verification::Verified + { + return Err(TransportPublishError::InvalidSignedEvent( + "signature verification failed".to_owned(), + )); + } + Ok(signed_event) } fn request_intent_fingerprint( principal_id: &str, canonical_event_json: &str, - request: &TransportPublishEventRequest, + request: &EventRequest, effective_timeout_ms: u64, ) -> Result<String, TransportPublishError> { #[derive(Serialize)] struct FingerprintInput<'a> { principal_id: &'a str, canonical_event_json: &'a str, - target_policy: &'a TransportPublishTargetPolicy, - delivery_policy: &'a TransportPublishDeliveryPolicy, + target_policy: &'a TargetPolicy, + delivery_policy: &'a DeliveryPolicy, effective_timeout_ms: u64, } @@ -2657,7 +2638,7 @@ fn effective_publish_timeout_ms( fn push_resolved_relay( targets: &mut Vec<ResolvedPublishRelay>, url: RadrootsRelayUrl, - source: TransportPublishTargetSource, + source: TargetSource, metadata: PublishTargetMetadata, ) { if !targets @@ -2673,29 +2654,29 @@ fn push_resolved_relay( } } -fn reticulum_unavailable_outcome(target: &TransportPublishTarget) -> TransportPublishTargetOutcome { - TransportPublishTargetOutcome { +fn reticulum_unavailable_outcome(target: &Target) -> TargetOutcome { + TargetOutcome { transport_kind: TRANSPORT_KIND_RETICULUM.to_owned(), endpoint_uri: target.endpoint_uri.trim().to_owned(), target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), - source: TransportPublishTargetSource::Reticulum, + source: TargetSource::Reticulum, attempted: false, - outcome_kind: TransportPublishOutcomeKind::DeferredUntilImplemented, + outcome_kind: OutcomeKind::DeferredUntilImplemented, message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), latency_ms: None, } } -fn unsupported_transport_outcome(target: &TransportPublishTarget) -> TransportPublishTargetOutcome { - TransportPublishTargetOutcome { +fn unsupported_transport_outcome(target: &Target) -> TargetOutcome { + TargetOutcome { transport_kind: target.transport_kind.trim().to_owned(), endpoint_uri: target.endpoint_uri.trim().to_owned(), target_scope: target.target_scope.clone(), target_label: target.target_label.clone(), - source: TransportPublishTargetSource::Request, + source: TargetSource::Request, attempted: false, - outcome_kind: TransportPublishOutcomeKind::Unsupported, + outcome_kind: OutcomeKind::Unsupported, message: Some("transport kind is not supported by radrootsd transport publish".to_owned()), latency_ms: None, } @@ -2703,43 +2684,36 @@ fn unsupported_transport_outcome(target: &TransportPublishTarget) -> TransportPu fn relay_resolution_connection_failure( relay_url: impl Into<String>, - source: TransportPublishTargetSource, + source: TargetSource, metadata: &PublishTargetMetadata, message: impl Into<String>, -) -> TransportPublishTargetOutcome { - TransportPublishTargetOutcome { +) -> TargetOutcome { + TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), endpoint_uri: relay_url.into(), target_scope: metadata.target_scope.clone(), target_label: metadata.target_label.clone(), source, attempted: false, - outcome_kind: TransportPublishOutcomeKind::ConnectionFailed, + outcome_kind: OutcomeKind::ConnectionFailed, message: Some(message.into()), latency_ms: None, } } -fn target_snapshots_from_resolution( - resolution: &PublishRelayResolution, -) -> Vec<TransportPublishTargetOutcome> { +fn target_snapshots_from_resolution(resolution: &PublishRelayResolution) -> Vec<TargetOutcome> { let mut snapshots = resolution.outcomes.clone(); - snapshots.extend( - resolution - .targets - .iter() - .map(|target| TransportPublishTargetOutcome { - transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), - endpoint_uri: target.url.as_str().to_owned(), - target_scope: target.target_scope.clone(), - target_label: target.target_label.clone(), - source: target.source, - attempted: true, - outcome_kind: TransportPublishOutcomeKind::ConnectionFailed, - message: Some("publish_attempt_interrupted".to_owned()), - latency_ms: None, - }), - ); + snapshots.extend(resolution.targets.iter().map(|target| TargetOutcome { + transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), + endpoint_uri: target.url.as_str().to_owned(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + source: target.source, + attempted: true, + outcome_kind: OutcomeKind::ConnectionFailed, + message: Some("publish_attempt_interrupted".to_owned()), + latency_ms: None, + })); snapshots } @@ -2772,9 +2746,7 @@ fn relay_url_policy(config: &TransportPublishConfig) -> RadrootsRelayUrlPolicy { } } -fn author_write_relays_from_nip65_event( - event: &radroots_nostr::prelude::RadrootsNostrEvent, -) -> Vec<String> { +fn author_write_relays_from_nip65_event(event: &crate::host_nostr::Event) -> Vec<String> { event .tags .iter() @@ -2799,11 +2771,11 @@ fn publish_outcomes_from_receipt( receipt: RadrootsRelayPublishRelayReceipt, targets: &[ResolvedPublishRelay], latency_ms: Option<u64>, -) -> Vec<TransportPublishTargetOutcome> { +) -> Vec<TargetOutcome> { targets .iter() .filter(|target| target.url.as_str() == receipt.relay_url.as_str()) - .map(|target| TransportPublishTargetOutcome { + .map(|target| TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), endpoint_uri: receipt.relay_url.clone(), target_scope: target.target_scope.clone(), @@ -2817,34 +2789,28 @@ fn publish_outcomes_from_receipt( .collect() } -fn publish_outcome_kind(kind: RadrootsRelayOutcomeKind) -> TransportPublishOutcomeKind { +fn publish_outcome_kind(kind: RadrootsRelayOutcomeKind) -> OutcomeKind { match kind { - RadrootsRelayOutcomeKind::Accepted => TransportPublishOutcomeKind::Accepted, - RadrootsRelayOutcomeKind::DuplicateAccepted => { - TransportPublishOutcomeKind::DuplicateAccepted - } - RadrootsRelayOutcomeKind::Blocked => TransportPublishOutcomeKind::Blocked, - RadrootsRelayOutcomeKind::RateLimited => TransportPublishOutcomeKind::RateLimited, - RadrootsRelayOutcomeKind::Invalid => TransportPublishOutcomeKind::Invalid, - RadrootsRelayOutcomeKind::PowRequired => TransportPublishOutcomeKind::PowRequired, - RadrootsRelayOutcomeKind::Restricted => TransportPublishOutcomeKind::Restricted, - RadrootsRelayOutcomeKind::AuthRequired => TransportPublishOutcomeKind::AuthRequired, - RadrootsRelayOutcomeKind::Muted => TransportPublishOutcomeKind::Muted, - RadrootsRelayOutcomeKind::Unsupported => TransportPublishOutcomeKind::Unsupported, - RadrootsRelayOutcomeKind::PaymentRequired => TransportPublishOutcomeKind::PaymentRequired, - RadrootsRelayOutcomeKind::Error => TransportPublishOutcomeKind::Error, - RadrootsRelayOutcomeKind::Timeout => TransportPublishOutcomeKind::Timeout, - RadrootsRelayOutcomeKind::ConnectionFailed => TransportPublishOutcomeKind::ConnectionFailed, - RadrootsRelayOutcomeKind::RelayUrlRejected => TransportPublishOutcomeKind::TargetRejected, - RadrootsRelayOutcomeKind::SkippedAlreadyAccepted => { - TransportPublishOutcomeKind::SkippedAlreadyAccepted - } - RadrootsRelayOutcomeKind::Unknown => TransportPublishOutcomeKind::Unknown, + RadrootsRelayOutcomeKind::Accepted => OutcomeKind::Accepted, + RadrootsRelayOutcomeKind::DuplicateAccepted => OutcomeKind::DuplicateAccepted, + RadrootsRelayOutcomeKind::Blocked => OutcomeKind::Blocked, + RadrootsRelayOutcomeKind::RateLimited => OutcomeKind::RateLimited, + RadrootsRelayOutcomeKind::Invalid => OutcomeKind::Invalid, + RadrootsRelayOutcomeKind::PowRequired => OutcomeKind::PowRequired, + RadrootsRelayOutcomeKind::Restricted => OutcomeKind::Restricted, + RadrootsRelayOutcomeKind::AuthRequired => OutcomeKind::AuthRequired, + RadrootsRelayOutcomeKind::Muted => OutcomeKind::Muted, + RadrootsRelayOutcomeKind::Unsupported => OutcomeKind::Unsupported, + RadrootsRelayOutcomeKind::PaymentRequired => OutcomeKind::PaymentRequired, + RadrootsRelayOutcomeKind::Error => OutcomeKind::Error, + RadrootsRelayOutcomeKind::Timeout => OutcomeKind::Timeout, + RadrootsRelayOutcomeKind::ConnectionFailed => OutcomeKind::ConnectionFailed, + RadrootsRelayOutcomeKind::Unknown => OutcomeKind::Unknown, } } fn target_outcome_fingerprint( - target: &TransportPublishTargetOutcome, + target: &TargetOutcome, index: usize, ) -> Result<RadrootsTransportTargetFingerprint, TransportPublishError> { let transport_kind = RadrootsTransportKind::parse_canonical(target.transport_kind.as_str()) @@ -2884,7 +2850,7 @@ fn target_outcome_fingerprint( "target outcome {index} fingerprint failed: {error}" )) })?; - Ok(target.fingerprint) + Ok(target.fingerprint().clone()) } fn transport_target_from_outcome_parts( @@ -2906,40 +2872,41 @@ fn transport_target_from_outcome_parts( RadrootsTransportKind::Local => { RadrootsTransportTarget::local_with_metadata(endpoint_uri, scope, label) } + _ => Err(radroots_transport::RadrootsTransportError::InvalidTargetUri), } } fn validate_delivery_policy_for_resolution( - delivery_policy: &TransportPublishDeliveryPolicy, + delivery_policy: &DeliveryPolicy, resolution: &PublishRelayResolution, ) -> Result<(), TransportPublishError> { - if !matches!( - delivery_policy, - TransportPublishDeliveryPolicy::RequiredTargets { .. } - ) { + let DeliveryPolicy::RequiredTargets { targets } = delivery_policy else { return Ok(()); - } + }; let target_fingerprints = resolution.target_fingerprints()?; - delivery_policy - .validate_target_membership(target_fingerprints.as_slice()) - .map_err(|error| { - TransportPublishError::InvalidSignedEvent(format!( - "publish request delivery policy validation failed: {error}" - )) - }) + if targets.iter().any(|required| { + !target_fingerprints + .iter() + .any(|actual| actual.as_str() == required.as_str()) + }) { + return Err(TransportPublishError::InvalidSignedEvent( + "publish request requires a target outside the resolved set".to_owned(), + )); + } + Ok(()) } fn required_outcomes_for_policy<'a>( - required_targets: &[RadrootsTransportTargetFingerprint], - outcomes: &'a [TransportPublishTargetOutcome], -) -> Vec<&'a TransportPublishTargetOutcome> { + required_targets: &[ProtocolTargetFingerprint], + outcomes: &'a [TargetOutcome], +) -> Vec<&'a TargetOutcome> { required_targets .iter() .filter_map(|required| { outcomes.iter().enumerate().find_map(|(index, outcome)| { target_outcome_fingerprint(outcome, index) .ok() - .filter(|fingerprint| fingerprint == required) + .filter(|fingerprint| fingerprint.as_str() == required.as_str()) .map(|_| outcome) }) }) @@ -2947,24 +2914,20 @@ fn required_outcomes_for_policy<'a>( } fn satisfaction_policy_from_delivery_policy( - delivery_policy: &TransportPublishDeliveryPolicy, + delivery_policy: &DeliveryPolicy, target_count: usize, nostr_targets: &[ResolvedPublishRelay], ) -> Result<RadrootsTransportSatisfactionPolicy, TransportPublishError> { match delivery_policy { - TransportPublishDeliveryPolicy::Any => { - Ok(RadrootsTransportSatisfactionPolicy::any_accepted()) - } - TransportPublishDeliveryPolicy::All => { - Ok(RadrootsTransportSatisfactionPolicy::all_accepted()) - } - TransportPublishDeliveryPolicy::Quorum { quorum } => { + DeliveryPolicy::Any => Ok(RadrootsTransportSatisfactionPolicy::any_accepted()), + DeliveryPolicy::All => Ok(RadrootsTransportSatisfactionPolicy::all_accepted()), + DeliveryPolicy::Quorum { quorum } => { let required = (*quorum).min(target_count).min(nostr_targets.len()).max(1); Ok(RadrootsTransportSatisfactionPolicy::quorum_accepted( u16::try_from(required).unwrap_or(u16::MAX), )) } - TransportPublishDeliveryPolicy::RequiredTargets { targets } => { + DeliveryPolicy::RequiredTargets { targets } => { let nostr_required_targets = targets .iter() .filter_map(|required| { @@ -2972,7 +2935,7 @@ fn satisfaction_policy_from_delivery_policy( target .fingerprint() .ok() - .filter(|fingerprint| fingerprint == required) + .filter(|fingerprint| fingerprint.as_str() == required.as_str()) }) }) .collect::<Vec<_>>(); @@ -2990,12 +2953,12 @@ fn satisfaction_policy_from_delivery_policy( } fn delivery_status( - delivery_policy: &TransportPublishDeliveryPolicy, + delivery_policy: &DeliveryPolicy, target_count: usize, - outcomes: &[TransportPublishTargetOutcome], -) -> TransportPublishJobStatus { + outcomes: &[TargetOutcome], +) -> JobStatus { let (satisfied, status_outcomes) = match delivery_policy { - TransportPublishDeliveryPolicy::RequiredTargets { targets } => { + DeliveryPolicy::RequiredTargets { targets } => { let required_outcomes = required_outcomes_for_policy(targets, outcomes); let satisfied = required_outcomes.len() == targets.len() && required_outcomes @@ -3003,9 +2966,7 @@ fn delivery_status( .all(|outcome| outcome.outcome_kind.counts_toward_accepted_delivery()); (satisfied, required_outcomes) } - TransportPublishDeliveryPolicy::Any - | TransportPublishDeliveryPolicy::All - | TransportPublishDeliveryPolicy::Quorum { .. } => { + DeliveryPolicy::Any | DeliveryPolicy::All | DeliveryPolicy::Quorum { .. } => { let required = delivery_policy.required_target_count(target_count); let acknowledged = outcomes .iter() @@ -3018,37 +2979,36 @@ fn delivery_status( } }; if satisfied { - return TransportPublishJobStatus::DeliverySatisfied; + return JobStatus::DeliverySatisfied; } if status_outcomes .iter() .any(|outcome| outcome.outcome_kind.is_retryable()) { - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - } else if status_outcomes.iter().any(|outcome| { - outcome.outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented - }) && status_outcomes + JobStatus::DeliveryUnsatisfiedRetryable + } else if status_outcomes .iter() - .all(|outcome| !outcome.outcome_kind.is_terminal_failure()) + .any(|outcome| outcome.outcome_kind == OutcomeKind::DeferredUntilImplemented) + && status_outcomes + .iter() + .all(|outcome| !outcome.outcome_kind.is_terminal_failure()) { - TransportPublishJobStatus::DeliveryDeferredUntilImplemented + JobStatus::DeliveryDeferredUntilImplemented } else { - TransportPublishJobStatus::DeliveryUnsatisfiedTerminal + JobStatus::DeliveryUnsatisfiedTerminal } } -fn last_error_for_status(status: TransportPublishJobStatus) -> Option<&'static str> { +fn last_error_for_status(status: JobStatus) -> Option<&'static str> { match status { - TransportPublishJobStatus::DeliverySatisfied => None, - TransportPublishJobStatus::Rejected => Some("no_transport_publish_targets"), - TransportPublishJobStatus::DeliveryDeferred => Some("delivery_deferred_until_implemented"), - TransportPublishJobStatus::DeliveryDeferredUntilImplemented => { - Some("delivery_deferred_until_implemented") - } - TransportPublishJobStatus::Accepted - | TransportPublishJobStatus::Publishing - | TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal => Some("delivery_unsatisfied"), + JobStatus::DeliverySatisfied => None, + JobStatus::Rejected => Some("no_transport_publish_targets"), + JobStatus::DeliveryDeferred => Some("delivery_deferred_until_implemented"), + JobStatus::DeliveryDeferredUntilImplemented => Some("delivery_deferred_until_implemented"), + JobStatus::Accepted + | JobStatus::Publishing + | JobStatus::DeliveryUnsatisfiedRetryable + | JobStatus::DeliveryUnsatisfiedTerminal => Some("delivery_unsatisfied"), } } @@ -3258,22 +3218,24 @@ mod tests { use crate::app::config::{ NostrRelayUrlPolicy, TransportPublishConfig, TransportPublishNostrConfig, }; + use crate::app::identity_storage::DaemonIdentity; + use crate::host_nostr::Timestamp; + use crate::transport::relay_publish::{ + MockRelayPublishAdapter as RadrootsMockRelayPublishAdapter, + RelayOutcome as RadrootsRelayOutcome, + }; use nostr::JsonUtil; use nostr::{EventBuilder, Kind, Tag}; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::RadrootsNostrTimestamp; - use radroots_transport::{ - RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - RadrootsTransportTarget, + use radroots_protocol::radrootsd::transport_publish::v5::{ + DeliveryPolicy, EventRequest, JobStatus, NostrTargetSourcePolicy, OutcomeKind, + ReticulumBehavior, Target, TargetFingerprint as ProtocolTargetFingerprint, TargetOutcome, + TargetPolicy, TargetPolicyName, TargetSource, }; - use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome}; - use radroots_transport_publish_protocol::{ - NostrPublishTargetSourcePolicy, TransportPublishDeliveryPolicy, - TransportPublishEventRequest, TransportPublishJobStatus, TransportPublishOutcomeKind, - TransportPublishReticulumBehavior, TransportPublishTarget, TransportPublishTargetOutcome, - TransportPublishTargetPolicy, TransportPublishTargetPolicyName, - TransportPublishTargetSource, + use radroots_protocol::radrootsd::transport_publish::v5::{ + RETICULUM_ENDPOINT_URI as RADROOTS_RETICULUM_ENDPOINT_URI, + RETICULUM_UNAVAILABLE_MESSAGE as RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, }; + use radroots_transport::RadrootsTransportTarget; use sqlx::Row; use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; use std::collections::BTreeMap; @@ -3302,14 +3264,14 @@ mod tests { ) } - fn request(pubkey: &str, kind: u32) -> TransportPublishEventRequest { - TransportPublishEventRequest { + fn request(pubkey: &str, kind: u32) -> EventRequest { + EventRequest { raw_event_json: raw_event_json(pubkey, kind), - target_policy: TransportPublishTargetPolicy::nostr( - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, + target_policy: TargetPolicy::nostr( + NostrTargetSourcePolicy::DaemonDefaultOnly, Vec::new(), ), - delivery_policy: TransportPublishDeliveryPolicy::Any, + delivery_policy: DeliveryPolicy::Any, idempotency_key: Some("idem-1".to_owned()), timeout_ms: None, } @@ -3383,12 +3345,12 @@ mod tests { .collect() } - fn signed_event(identity: &RadrootsIdentity, content: &str) -> String { + fn signed_event(identity: &DaemonIdentity, content: &str) -> String { // Transport tests require an already-signed wire fixture; they do not // exercise a Radroots product-authoring boundary. let event = EventBuilder::new(Kind::Custom(30_402), content) .tag(Tag::identifier("listing-1")) - .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000)) + .custom_created_at(Timestamp::from_secs(1_700_000_000)) .sign_with_keys(identity.keys()) .expect("signed event"); event.as_json() @@ -3408,13 +3370,13 @@ mod tests { fn publish_request( raw_event_json: String, relays: Vec<String>, - source_policy: NostrPublishTargetSourcePolicy, - delivery_policy: TransportPublishDeliveryPolicy, + source_policy: NostrTargetSourcePolicy, + delivery_policy: DeliveryPolicy, idempotency_key: Option<&str>, - ) -> TransportPublishEventRequest { - TransportPublishEventRequest { + ) -> EventRequest { + EventRequest { raw_event_json, - target_policy: TransportPublishTargetPolicy::nostr(source_policy, relays), + target_policy: TargetPolicy::nostr(source_policy, relays), delivery_policy, idempotency_key: idempotency_key.map(str::to_owned), timeout_ms: Some(5_000), @@ -3423,58 +3385,50 @@ mod tests { fn reticulum_publish_request( raw_event_json: String, - behavior: TransportPublishReticulumBehavior, - ) -> TransportPublishEventRequest { - TransportPublishEventRequest { + behavior: ReticulumBehavior, + ) -> EventRequest { + EventRequest { raw_event_json, - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::reticulum(behavior), - ]), - delivery_policy: TransportPublishDeliveryPolicy::Any, + target_policy: TargetPolicy::explicit_targets(vec![Target::reticulum(behavior)]), + delivery_policy: DeliveryPolicy::Any, idempotency_key: None, timeout_ms: Some(5_000), } } - fn interrupted_target_snapshot( - endpoint_uri: &str, - source: TransportPublishTargetSource, - ) -> TransportPublishTargetOutcome { - TransportPublishTargetOutcome { + fn interrupted_target_snapshot(endpoint_uri: &str, source: TargetSource) -> TargetOutcome { + TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), endpoint_uri: endpoint_uri.to_owned(), target_scope: None, target_label: None, source, attempted: true, - outcome_kind: TransportPublishOutcomeKind::ConnectionFailed, + outcome_kind: OutcomeKind::ConnectionFailed, message: Some("publish_attempt_interrupted".to_owned()), latency_ms: None, } } - fn accepted_target_outcome( - endpoint_uri: &str, - source: TransportPublishTargetSource, - ) -> TransportPublishTargetOutcome { - TransportPublishTargetOutcome { + fn accepted_target_outcome(endpoint_uri: &str, source: TargetSource) -> TargetOutcome { + TargetOutcome { transport_kind: TRANSPORT_KIND_NOSTR.to_owned(), endpoint_uri: endpoint_uri.to_owned(), target_scope: None, target_label: None, source, attempted: true, - outcome_kind: TransportPublishOutcomeKind::Accepted, + outcome_kind: OutcomeKind::Accepted, message: None, latency_ms: Some(12), } } fn scoped_target_outcome( - mut outcome: TransportPublishTargetOutcome, + mut outcome: TargetOutcome, target_scope: &str, target_label: Option<&str>, - ) -> TransportPublishTargetOutcome { + ) -> TargetOutcome { outcome.target_scope = Some(target_scope.to_owned()); outcome.target_label = target_label.map(str::to_owned); outcome @@ -3487,11 +3441,9 @@ mod tests { token_hash: hash_bearer_token(generate_bearer_token().as_str()), allowed_pubkeys: vec![pubkey.to_owned()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, @@ -3542,7 +3494,7 @@ mod tests { fn principal( proxy: &TransportPublish, pubkey: String, - nostr_source_policies: Vec<NostrPublishTargetSourcePolicy>, + nostr_source_policies: Vec<NostrTargetSourcePolicy>, allow_request_targets: bool, visibility: PublishJobVisibility, ) -> PublishPrincipal { @@ -3553,7 +3505,7 @@ mod tests { token_hash: hash_bearer_token(generate_bearer_token().as_str()), allowed_pubkeys: vec![pubkey], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), allowed_nostr_source_policies: nostr_source_policies, allow_request_targets, @@ -3592,7 +3544,7 @@ mod tests { token_hash: hash_bearer_token(generate_bearer_token().as_str()), allowed_pubkeys: vec![pubkey], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::ExplicitTargets], + allowed_target_policies: vec![TargetPolicyName::ExplicitTargets], allowed_explicit_transport_kinds, allowed_nostr_source_policies: Vec::new(), allow_request_targets: true, @@ -3620,7 +3572,7 @@ mod tests { token_hash: hash_bearer_token(generate_bearer_token().as_str()), allowed_pubkeys: vec!["a".repeat(64)], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::ExplicitTargets], + allowed_target_policies: vec![TargetPolicyName::ExplicitTargets], allowed_explicit_transport_kinds: Vec::new(), allowed_nostr_source_policies: Vec::new(), allow_request_targets: true, @@ -3663,12 +3615,11 @@ mod tests { )); let mut nostr_policy_with_explicit_kinds = base; - nostr_policy_with_explicit_kinds.allowed_target_policies = - vec![TransportPublishTargetPolicyName::Nostr]; + nostr_policy_with_explicit_kinds.allowed_target_policies = vec![TargetPolicyName::Nostr]; nostr_policy_with_explicit_kinds.allowed_explicit_transport_kinds = vec![TRANSPORT_KIND_NOSTR.to_owned()]; nostr_policy_with_explicit_kinds.allowed_nostr_source_policies = - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly]; + vec![NostrTargetSourcePolicy::DaemonDefaultOnly]; assert!(matches!( store.create_principal(nostr_policy_with_explicit_kinds), Err(TransportPublishError::InvalidScope(message)) @@ -3700,7 +3651,7 @@ mod tests { impl super::PublishRelayResolver for StaticPublishRelayResolver { fn resolve<'a>( &'a self, - url: &'a radroots_transport_nostr::RadrootsRelayUrl, + url: &'a super::RadrootsRelayUrl, ) -> super::PublishRelayResolveFuture<'a> { Box::pin(async move { match self.results.get(url.as_str()) { @@ -3749,7 +3700,7 @@ mod tests { fn nostr_source_policy_parser_accepts_contract_values() { assert_eq!( parse_nostr_source_policy("explicit_only").expect("policy"), - NostrPublishTargetSourcePolicy::ExplicitOnly + NostrTargetSourcePolicy::ExplicitOnly ); assert!(parse_nostr_source_policy("unknown").is_err()); } @@ -3759,19 +3710,17 @@ mod tests { let store = TransportPublishStore::memory().expect("store"); let token = generate_bearer_token(); let token_hash = hash_bearer_token(token.as_str()); - let accepted_identity = RadrootsIdentity::generate(); - let denied_identity = RadrootsIdentity::generate(); + let accepted_identity = DaemonIdentity::generate(); + let denied_identity = DaemonIdentity::generate(); let principal = store .create_principal(PublishPrincipalInit { label: "tester".to_owned(), token_hash: token_hash.clone(), allowed_pubkeys: vec![accepted_identity.public_key_hex()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, @@ -3788,8 +3737,8 @@ mod tests { let denied = publish_request( signed_event(&denied_identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ); let denied_signed = @@ -3799,8 +3748,8 @@ mod tests { let accepted = publish_request( signed_event(&accepted_identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ); let accepted_signed = super::signed_event_from_raw_json(accepted.raw_event_json.as_str()) @@ -3818,7 +3767,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![interrupted_target_snapshot( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); @@ -3833,7 +3782,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![interrupted_target_snapshot( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("dedupe"); @@ -3864,17 +3813,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], None, ) @@ -3917,7 +3866,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect_err("invalid dedupe"), @@ -3942,17 +3891,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], None, ) @@ -3996,7 +3945,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect_err("invalid dedupe"), @@ -4021,17 +3970,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], None, ) @@ -4074,7 +4023,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect_err("invalid dedupe"), @@ -4098,17 +4047,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], None, ) @@ -4151,7 +4100,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect_err("invalid dedupe"), @@ -4165,10 +4114,7 @@ mod tests { let pubkey = "a".repeat(64); let principal = store_principal(&store, pubkey.as_str()); let mut request = request(pubkey.as_str(), 30_402); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget::nostr( - RELAY_PRIMARY, - )]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target::nostr(RELAY_PRIMARY)]); let response = store .record_publish_job(PublishJobInsert { principal_id: principal.principal_id.clone(), @@ -4179,17 +4125,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_PRIMARY, - TransportPublishTargetSource::Request, + TargetSource::Request, )], }) .expect("record job"); store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![accepted_target_outcome( RELAY_SECONDARY, - TransportPublishTargetSource::Request, + TargetSource::Request, )], None, ) @@ -4209,8 +4155,8 @@ mod tests { let pubkey = "a".repeat(64); let principal = store_principal(&store, pubkey.as_str()); let mut request = request(pubkey.as_str(), 30_402); - request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY) + request.target_policy = TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY) .with_scope("farm.local") .with_label("Farm relay"), ]); @@ -4223,7 +4169,7 @@ mod tests { request_fingerprint: "fingerprint-explicit-scope-drift".to_owned(), effective_target_count: 1, target_snapshots: vec![scoped_target_outcome( - accepted_target_outcome(RELAY_PRIMARY, TransportPublishTargetSource::Request), + accepted_target_outcome(RELAY_PRIMARY, TargetSource::Request), "farm.local", Some("Farm relay"), )], @@ -4232,9 +4178,9 @@ mod tests { store .complete_publish_job( response.job.job_id.as_str(), - TransportPublishJobStatus::DeliverySatisfied, + JobStatus::DeliverySatisfied, vec![scoped_target_outcome( - accepted_target_outcome(RELAY_PRIMARY, TransportPublishTargetSource::Request), + accepted_target_outcome(RELAY_PRIMARY, TargetSource::Request), "farm.remote", Some("Farm relay"), )], @@ -4265,11 +4211,9 @@ mod tests { token_hash, allowed_pubkeys: vec![pubkey.clone()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, @@ -4285,20 +4229,17 @@ mod tests { effective_target_count: 1, target_snapshots: vec![interrupted_target_snapshot( RELAY_PRIMARY, - TransportPublishTargetSource::DaemonDefault, + TargetSource::DaemonDefault, )], }) .expect("record job"); - assert_eq!(response.job.status, TransportPublishJobStatus::Publishing); + assert_eq!(response.job.status, JobStatus::Publishing); (response.job.job_id, principal) }; let reopened = TransportPublishStore::open(database_path).expect("reopen store"); let recovered = reopened.job_by_id(job_id.as_str()).expect("recovered job"); - assert_eq!( - recovered.status, - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - ); + assert_eq!(recovered.status, JobStatus::DeliveryUnsatisfiedRetryable); assert_eq!( recovered.last_error.as_deref(), Some("publish_attempt_interrupted") @@ -4307,7 +4248,7 @@ mod tests { assert_eq!(recovered.targets.len(), 1); assert_eq!( recovered.targets[0].outcome_kind, - TransportPublishOutcomeKind::ConnectionFailed + OutcomeKind::ConnectionFailed ); recovered.validate().expect("valid recovered job"); let listed = reopened @@ -4326,8 +4267,8 @@ mod tests { let store = TransportPublishStore::open(database_path.clone()).expect("store"); let principal = store_principal(&store, pubkey.as_str()); let mut request = request(pubkey.as_str(), 30_402); - request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY) + request.target_policy = TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY) .with_scope("farm.local") .with_label("Farm relay"), ]); @@ -4340,10 +4281,7 @@ mod tests { request_fingerprint: "fingerprint-interrupted-scoped".to_owned(), effective_target_count: 1, target_snapshots: vec![scoped_target_outcome( - interrupted_target_snapshot( - RELAY_PRIMARY, - TransportPublishTargetSource::Request, - ), + interrupted_target_snapshot(RELAY_PRIMARY, TargetSource::Request), "farm.local", Some("Farm relay"), )], @@ -4354,10 +4292,7 @@ mod tests { let reopened = TransportPublishStore::open(database_path).expect("reopen store"); let recovered = reopened.job_by_id(job_id.as_str()).expect("recovered job"); - assert_eq!( - recovered.status, - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - ); + assert_eq!(recovered.status, JobStatus::DeliveryUnsatisfiedRetryable); assert_eq!(recovered.targets.len(), 1); assert_eq!( recovered.targets[0].target_scope.as_deref(), @@ -4390,9 +4325,8 @@ mod tests { let store = TransportPublishStore::open(database_path.clone()).expect("store"); let principal = store_principal(&store, pubkey.as_str()); let mut request = request(pubkey.as_str(), 30_402); - request.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - ]); + request.target_policy = + TargetPolicy::explicit_targets(vec![Target::nostr(RELAY_PRIMARY)]); let response = store .record_publish_job(PublishJobInsert { principal_id: principal.principal_id.clone(), @@ -4403,7 +4337,7 @@ mod tests { effective_target_count: 1, target_snapshots: vec![accepted_target_outcome( RELAY_SECONDARY, - TransportPublishTargetSource::Request, + TargetSource::Request, )], }) .expect("record job"); @@ -4442,11 +4376,9 @@ mod tests { token_hash, allowed_pubkeys: vec![pubkey.clone()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, @@ -4484,9 +4416,7 @@ mod tests { .bind(principal.principal_id.as_str()) .bind("idem-missing-snapshot") .bind("fingerprint-missing-snapshot") - .bind( - serde_json::to_string(&TransportPublishJobStatus::Publishing).expect("status"), - ) + .bind(serde_json::to_string(&JobStatus::Publishing).expect("status")) .bind("0".repeat(64)) .bind(pubkey.as_str()) .bind(30_402_i64) @@ -4512,7 +4442,7 @@ mod tests { let recovered = reopened .job_by_id("job-missing-snapshot") .expect("recovered job"); - assert_eq!(recovered.status, TransportPublishJobStatus::Rejected); + assert_eq!(recovered.status, JobStatus::Rejected); assert_eq!( recovered.last_error.as_deref(), Some("publish_attempt_interrupted_missing_target_snapshot") @@ -4599,12 +4529,9 @@ mod tests { .bind(token_hash.as_str()) .bind(serde_json::to_string(&vec!["a".repeat(64)]).expect("pubkeys")) .bind(serde_json::to_string(&vec![30_402]).expect("kinds")) + .bind(serde_json::to_string(&vec![TargetPolicyName::Nostr]).expect("policies")) .bind( - serde_json::to_string(&vec![TransportPublishTargetPolicyName::Nostr]) - .expect("policies"), - ) - .bind( - serde_json::to_string(&vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly]) + serde_json::to_string(&vec![NostrTargetSourcePolicy::DaemonDefaultOnly]) .expect("source policies"), ) .bind(false) @@ -4832,12 +4759,12 @@ mod tests { #[tokio::test] async fn publish_event_verifies_and_records_daemon_default_outcome() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -4849,8 +4776,8 @@ mod tests { publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-valid"), ), ) @@ -4858,28 +4785,22 @@ mod tests { .expect("publish"); assert!(!response.deduplicated); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.target_count, 1); assert_eq!(response.job.acknowledged_count, 1); assert_eq!(response.job.targets[0].endpoint_uri, RELAY_PRIMARY); - assert_eq!( - response.job.targets[0].source, - TransportPublishTargetSource::DaemonDefault - ); + assert_eq!(response.job.targets[0].source, TargetSource::DaemonDefault); assert_eq!(adapter.captured_raw_events(), vec![raw_event]); } #[tokio::test] async fn publish_event_rejects_tampered_content_before_publish() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -4894,8 +4815,8 @@ mod tests { publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) @@ -4908,12 +4829,12 @@ mod tests { #[tokio::test] async fn publish_event_rejects_wrong_signature_before_publish() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -4930,8 +4851,8 @@ mod tests { publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) @@ -4940,19 +4861,19 @@ mod tests { assert!(matches!( error, - TransportPublishError::SignedEventSignature(_) + TransportPublishError::InvalidSignedEvent(_) )); assert!(adapter.captured_raw_events().is_empty()); } #[tokio::test] async fn publish_event_rejects_malformed_wire_fields() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -4966,8 +4887,8 @@ mod tests { publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) @@ -4980,12 +4901,12 @@ mod tests { #[tokio::test] async fn publish_event_uses_explicit_request_relays_when_allowed() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_SECONDARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault], + vec![NostrTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault], true, PublishJobVisibility::Own, ); @@ -4995,28 +4916,22 @@ mod tests { publish_request( signed_event(&identity, "{}"), vec![RELAY_PRIMARY.to_owned()], - NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.targets[0].endpoint_uri, RELAY_PRIMARY); - assert_eq!( - response.job.targets[0].source, - TransportPublishTargetSource::Request - ); + assert_eq!(response.job.targets[0].source, TargetSource::Request); } #[tokio::test] async fn publish_event_uses_cached_nip65_author_write_before_defaults() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_SECONDARY])); proxy .store @@ -5028,7 +4943,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault], + vec![NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault], false, PublishJobVisibility::Own, ); @@ -5038,8 +4953,8 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault, + DeliveryPolicy::Any, None, ), ) @@ -5049,13 +4964,13 @@ mod tests { assert_eq!(response.job.targets[0].endpoint_uri, RELAY_PRIMARY); assert_eq!( response.job.targets[0].source, - TransportPublishTargetSource::NostrAuthorWrite + TargetSource::NostrAuthorWrite ); } #[tokio::test] - async fn publish_event_records_invalid_cached_author_write_relay() { - let identity = RadrootsIdentity::generate(); + async fn publish_event_discards_invalid_cached_author_write_relay() { + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_SECONDARY])); proxy .store @@ -5067,7 +4982,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault], + vec![NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault], false, PublishJobVisibility::Own, ); @@ -5077,50 +4992,36 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); let accepted = response .job .targets .iter() .find(|relay| relay.endpoint_uri == RELAY_PRIMARY) .expect("accepted author relay"); - assert_eq!( - accepted.source, - TransportPublishTargetSource::NostrAuthorWrite - ); + assert_eq!(accepted.source, TargetSource::NostrAuthorWrite); assert!(accepted.attempted); - let rejected = response - .job - .targets - .iter() - .find(|relay| relay.endpoint_uri == "not a cached relay") - .expect("rejected cached author relay"); - assert_eq!( - rejected.source, - TransportPublishTargetSource::NostrAuthorWrite - ); - assert_eq!( - rejected.outcome_kind, - TransportPublishOutcomeKind::TargetRejected + assert!( + response + .job + .targets + .iter() + .all(|relay| relay.endpoint_uri != "not a cached relay") ); - assert!(!rejected.attempted); assert_eq!(adapter.captured_raw_events().len(), 1); } #[tokio::test] - async fn publish_event_preserves_author_and_discovery_rejections_through_relay_selection() { - let identity = RadrootsIdentity::generate(); + async fn publish_event_discards_invalid_author_and_discovery_relays() { + let identity = DaemonIdentity::generate(); let mut config = config_with_defaults(vec![RELAY_SECONDARY]); config.nostr.author_relay_discovery_relays = vec!["not a discovery relay".to_owned()]; let (proxy, adapter) = transport_publish(config); @@ -5134,7 +5035,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault], + vec![NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault], false, PublishJobVisibility::Own, ); @@ -5144,65 +5045,33 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); let daemon_default = response .job .targets .iter() .find(|relay| relay.endpoint_uri == RELAY_SECONDARY) .expect("daemon default relay"); - assert_eq!( - daemon_default.source, - TransportPublishTargetSource::DaemonDefault - ); + assert_eq!(daemon_default.source, TargetSource::DaemonDefault); assert!(daemon_default.attempted); - let cached = response - .job - .targets - .iter() - .find(|relay| relay.endpoint_uri == "not a cached relay") - .expect("cached author rejection"); - assert_eq!( - cached.source, - TransportPublishTargetSource::NostrAuthorWrite - ); - assert_eq!( - cached.outcome_kind, - TransportPublishOutcomeKind::TargetRejected - ); - assert!(!cached.attempted); - let discovery = response - .job - .targets - .iter() - .find(|relay| relay.endpoint_uri == "not a discovery relay") - .expect("discovery relay rejection"); - assert_eq!( - discovery.source, - TransportPublishTargetSource::DaemonDefault - ); - assert_eq!( - discovery.outcome_kind, - TransportPublishOutcomeKind::TargetRejected - ); - assert!(!discovery.attempted); + assert!(response.job.targets.iter().all(|relay| { + relay.endpoint_uri != "not a cached relay" + && relay.endpoint_uri != "not a discovery relay" + })); assert_eq!(adapter.captured_raw_events().len(), 1); } #[tokio::test] - async fn publish_event_preserves_discovery_and_discovered_author_rejections() { - let identity = RadrootsIdentity::generate(); + async fn publish_event_preserves_valid_discovery_rejections_only() { + let identity = DaemonIdentity::generate(); let mut config = config_with_defaults(vec![RELAY_PRIMARY]); config.nostr.author_relay_discovery_relays = vec![RELAY_PRIMARY.to_owned(), RELAY_FORBIDDEN.to_owned()]; @@ -5222,7 +5091,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault], + vec![NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault], false, PublishJobVisibility::Own, ); @@ -5232,70 +5101,50 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::AuthorWriteThenDaemonDefault, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::AuthorWriteThenDaemonDefault, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); let accepted = response .job .targets .iter() .find(|relay| relay.endpoint_uri == RELAY_SECONDARY) .expect("discovered author relay"); - assert_eq!( - accepted.source, - TransportPublishTargetSource::NostrAuthorWrite - ); + assert_eq!(accepted.source, TargetSource::NostrAuthorWrite); assert!(accepted.attempted); - let discovered = response - .job - .targets - .iter() - .find(|relay| relay.endpoint_uri == "not a discovered author relay") - .expect("discovered author rejection"); - assert_eq!( - discovered.source, - TransportPublishTargetSource::NostrAuthorWrite - ); - assert_eq!( - discovered.outcome_kind, - TransportPublishOutcomeKind::TargetRejected + assert!( + response + .job + .targets + .iter() + .all(|relay| relay.endpoint_uri != "not a discovered author relay") ); - assert!(!discovered.attempted); let discovery = response .job .targets .iter() .find(|relay| relay.endpoint_uri == RELAY_FORBIDDEN) .expect("discovery relay rejection"); - assert_eq!( - discovery.source, - TransportPublishTargetSource::DaemonDefault - ); - assert_eq!( - discovery.outcome_kind, - TransportPublishOutcomeKind::TargetRejected - ); + assert_eq!(discovery.source, TargetSource::DaemonDefault); + assert_eq!(discovery.outcome_kind, OutcomeKind::TargetRejected); assert!(!discovery.attempted); assert_eq!(adapter.captured_raw_events().len(), 1); } #[tokio::test] async fn publish_event_records_no_transport_publish_targets_failure() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -5305,15 +5154,15 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!(response.job.status, TransportPublishJobStatus::Rejected); + assert_eq!(response.job.status, JobStatus::Rejected); assert_eq!( response.job.last_error.as_deref(), Some("no_transport_publish_targets") @@ -5324,7 +5173,7 @@ mod tests { #[tokio::test] async fn publish_event_records_reticulum_unavailable_as_deferred_until_implemented() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); @@ -5333,7 +5182,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ), ) .await @@ -5341,7 +5190,7 @@ mod tests { assert_eq!( response.job.status, - TransportPublishJobStatus::DeliveryDeferredUntilImplemented + JobStatus::DeliveryDeferredUntilImplemented ); assert!(response.job.terminal); assert!(!response.job.delivery_satisfied); @@ -5354,7 +5203,7 @@ mod tests { assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::DeferredUntilImplemented + OutcomeKind::DeferredUntilImplemented ); assert_eq!( response.job.targets[0].message.as_deref(), @@ -5366,18 +5215,16 @@ mod tests { #[tokio::test] async fn publish_event_records_explicit_nostr_target_when_kind_allowed() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let event = signed_event(&identity, "{}"); let raw_event = event.clone(); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: event, - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - ]), - delivery_policy: TransportPublishDeliveryPolicy::Any, + target_policy: TargetPolicy::explicit_targets(vec![Target::nostr(RELAY_PRIMARY)]), + delivery_policy: DeliveryPolicy::Any, idempotency_key: None, timeout_ms: Some(5_000), }; @@ -5387,15 +5234,9 @@ mod tests { .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.targets.len(), 1); - assert_eq!( - response.job.targets[0].source, - TransportPublishTargetSource::Request - ); + assert_eq!(response.job.targets[0].source, TargetSource::Request); assert_eq!(response.job.targets[0].endpoint_uri, RELAY_PRIMARY); assert_eq!(response.job.targets[0].target_scope, None); assert_eq!(response.job.targets[0].target_label, None); @@ -5404,20 +5245,20 @@ mod tests { #[tokio::test] async fn publish_event_preserves_explicit_nostr_target_metadata_when_kind_allowed() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let event = signed_event(&identity, "{}"); let raw_event = event.clone(); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: event, - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY) + target_policy: TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY) .with_scope("farm.local") .with_label("Farm relay"), ]), - delivery_policy: TransportPublishDeliveryPolicy::Any, + delivery_policy: DeliveryPolicy::Any, idempotency_key: None, timeout_ms: Some(5_000), }; @@ -5427,10 +5268,7 @@ mod tests { .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.targets.len(), 1); assert_eq!(response.job.targets[0].endpoint_uri, RELAY_PRIMARY); assert_eq!( @@ -5441,33 +5279,30 @@ mod tests { response.job.targets[0].target_label.as_deref(), Some("Farm relay") ); - assert_eq!( - response.job.targets[0].source, - TransportPublishTargetSource::Request - ); + assert_eq!(response.job.targets[0].source, TargetSource::Request); assert_eq!(adapter.captured_raw_events(), vec![raw_event]); response.job.validate().expect("valid scoped job"); } #[tokio::test] async fn publish_event_records_scoped_targets_with_shared_relay_url() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let event = signed_event(&identity, "{}"); let raw_event = event.clone(); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: event, - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY) + target_policy: TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY) .with_scope("farm.a") .with_label("Farm A"), - TransportPublishTarget::nostr(RELAY_PRIMARY) + Target::nostr(RELAY_PRIMARY) .with_scope("farm.b") .with_label("Farm B"), ]), - delivery_policy: TransportPublishDeliveryPolicy::All, + delivery_policy: DeliveryPolicy::All, idempotency_key: None, timeout_ms: Some(5_000), }; @@ -5477,10 +5312,7 @@ mod tests { .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.targets.len(), 2); assert!( response @@ -5513,7 +5345,7 @@ mod tests { #[tokio::test] async fn publish_event_required_targets_do_not_count_optional_success() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let adapter = RadrootsMockRelayPublishAdapter::new() .with_outcome( RELAY_PRIMARY, @@ -5526,14 +5358,15 @@ mod tests { explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let required_target = RadrootsTransportTarget::nostr_relay(RELAY_PRIMARY).expect("required target"); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: signed_event(&identity, "{}"), - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - TransportPublishTarget::nostr(RELAY_SECONDARY), + target_policy: TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY), + Target::nostr(RELAY_SECONDARY), ]), - delivery_policy: TransportPublishDeliveryPolicy::required_targets(vec![ - required_target.fingerprint, + delivery_policy: DeliveryPolicy::required_targets(vec![ + ProtocolTargetFingerprint::parse(required_target.fingerprint().as_str()) + .expect("protocol target fingerprint"), ]) .expect("required targets"), idempotency_key: None, @@ -5545,10 +5378,7 @@ mod tests { .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliveryUnsatisfiedTerminal - ); + assert_eq!(response.job.status, JobStatus::DeliveryUnsatisfiedTerminal); assert!(!response.job.delivery_satisfied); assert_eq!(response.job.acknowledged_count, 1); assert_eq!(adapter.captured_raw_events().len(), 1); @@ -5560,7 +5390,7 @@ mod tests { #[tokio::test] async fn publish_event_required_targets_ignore_optional_retryable_failures() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let adapter = RadrootsMockRelayPublishAdapter::new() .with_outcome(RELAY_PRIMARY, RadrootsRelayOutcome::accepted()) .with_outcome( @@ -5573,14 +5403,15 @@ mod tests { explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let required_target = RadrootsTransportTarget::nostr_relay(RELAY_PRIMARY).expect("required target"); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: signed_event(&identity, "{}"), - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - TransportPublishTarget::nostr(RELAY_SECONDARY), + target_policy: TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY), + Target::nostr(RELAY_SECONDARY), ]), - delivery_policy: TransportPublishDeliveryPolicy::required_targets(vec![ - required_target.fingerprint, + delivery_policy: DeliveryPolicy::required_targets(vec![ + ProtocolTargetFingerprint::parse(required_target.fingerprint().as_str()) + .expect("protocol target fingerprint"), ]) .expect("required targets"), idempotency_key: None, @@ -5592,10 +5423,7 @@ mod tests { .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert!(response.job.delivery_satisfied); assert_eq!(response.job.acknowledged_count, 1); assert_eq!(response.job.retryable_count, 1); @@ -5608,19 +5436,18 @@ mod tests { #[tokio::test] async fn publish_event_rejects_required_target_not_in_resolved_set() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let stale_target = RadrootsTransportTarget::nostr_relay(RELAY_SECONDARY).expect("stale target"); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: signed_event(&identity, "{}"), - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - ]), - delivery_policy: TransportPublishDeliveryPolicy::required_targets(vec![ - stale_target.fingerprint, + target_policy: TargetPolicy::explicit_targets(vec![Target::nostr(RELAY_PRIMARY)]), + delivery_policy: DeliveryPolicy::required_targets(vec![ + ProtocolTargetFingerprint::parse(stale_target.fingerprint().as_str()) + .expect("protocol target fingerprint"), ]) .expect("required targets"), idempotency_key: None, @@ -5632,11 +5459,14 @@ mod tests { .await .expect_err("stale required target"); - assert!(matches!( - err, - TransportPublishError::InvalidSignedEvent(ref message) - if message.contains("required target") - )); + assert!( + matches!( + err, + TransportPublishError::InvalidSignedEvent(ref message) + if message.contains("requires a target") + ), + "{err:?}" + ); assert!(adapter.captured_raw_events().is_empty()); assert!( proxy @@ -5649,17 +5479,17 @@ mod tests { #[tokio::test] async fn publish_event_rejects_duplicate_explicit_targets_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); - let request = TransportPublishEventRequest { + let request = EventRequest { raw_event_json: signed_event(&identity, "{}"), - target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ - TransportPublishTarget::nostr(RELAY_PRIMARY), - TransportPublishTarget::nostr(RELAY_PRIMARY), + target_policy: TargetPolicy::explicit_targets(vec![ + Target::nostr(RELAY_PRIMARY), + Target::nostr(RELAY_PRIMARY), ]), - delivery_policy: TransportPublishDeliveryPolicy::Any, + delivery_policy: DeliveryPolicy::Any, idempotency_key: None, timeout_ms: Some(5_000), }; @@ -5686,7 +5516,7 @@ mod tests { #[tokio::test] async fn publish_event_rejects_explicit_target_kind_not_allowed_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal_with_kinds( &proxy, @@ -5700,7 +5530,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ), ) .await @@ -5719,7 +5549,7 @@ mod tests { #[tokio::test] async fn publish_event_records_reticulum_deferred_as_terminal_nonfailure() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); @@ -5728,7 +5558,7 @@ mod tests { &principal, reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::DeferDeliveryPlans, + ReticulumBehavior::DeferDeliveryPlans, ), ) .await @@ -5736,7 +5566,7 @@ mod tests { assert_eq!( response.job.status, - TransportPublishJobStatus::DeliveryDeferredUntilImplemented + JobStatus::DeliveryDeferredUntilImplemented ); assert!(response.job.terminal); assert!(!response.job.delivery_satisfied); @@ -5749,7 +5579,7 @@ mod tests { assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::DeferredUntilImplemented + OutcomeKind::DeferredUntilImplemented ); assert!(!response.job.targets[0].attempted); assert!(adapter.captured_raw_events().is_empty()); @@ -5757,22 +5587,21 @@ mod tests { #[tokio::test] async fn publish_event_rejects_noncanonical_reticulum_endpoint_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: "reticulum".to_owned(), - endpoint_uri: "reticulum:unavailable-alt".to_owned(), - target_scope: None, - target_label: None, - reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), - }]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target { + transport_kind: "reticulum".to_owned(), + endpoint_uri: "reticulum:unavailable-alt".to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: Some(ReticulumBehavior::RejectDeliveryAttempts), + }]); let err = proxy .publish_event(&principal, request) @@ -5792,25 +5621,24 @@ mod tests { #[tokio::test] async fn publish_event_rejects_reticulum_behavior_on_non_reticulum_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = publish_request( signed_event(&identity, "{}"), vec![RELAY_PRIMARY.to_owned()], - NostrPublishTargetSourcePolicy::ExplicitOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::ExplicitOnly, + DeliveryPolicy::Any, None, ); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: "nostr".to_owned(), - endpoint_uri: RELAY_PRIMARY.to_owned(), - target_scope: None, - target_label: None, - reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), - }]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target { + transport_kind: "nostr".to_owned(), + endpoint_uri: RELAY_PRIMARY.to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: Some(ReticulumBehavior::RejectDeliveryAttempts), + }]); let err = proxy .publish_event(&principal, request) @@ -5830,22 +5658,21 @@ mod tests { #[tokio::test] async fn publish_event_rejects_noncanonical_reticulum_kind_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: "Reticulum".to_owned(), - endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), - target_scope: None, - target_label: None, - reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), - }]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target { + transport_kind: "Reticulum".to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: Some(ReticulumBehavior::RejectDeliveryAttempts), + }]); let err = proxy .publish_event(&principal, request) @@ -5865,22 +5692,21 @@ mod tests { #[tokio::test] async fn publish_event_rejects_removed_execution_kind_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: removed_execution_kind_string(), - endpoint_uri: removed_execution_endpoint_uri(), - target_scope: None, - target_label: None, - reticulum_behavior: None, - }]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target { + transport_kind: removed_execution_kind_string(), + endpoint_uri: removed_execution_endpoint_uri(), + target_scope: None, + target_label: None, + reticulum_behavior: None, + }]); let err = proxy .publish_event(&principal, request) @@ -5900,22 +5726,21 @@ mod tests { #[tokio::test] async fn publish_event_rejects_removed_execution_target_before_recording_job() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = explicit_target_principal(&proxy, identity.public_key_hex(), PublishJobVisibility::Own); let mut request = reticulum_publish_request( signed_event(&identity, "{}"), - TransportPublishReticulumBehavior::RejectDeliveryAttempts, + ReticulumBehavior::RejectDeliveryAttempts, ); - request.target_policy = - TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { - transport_kind: removed_proxy_transport_kind_string(), - endpoint_uri: removed_execution_endpoint_uri(), - target_scope: None, - target_label: None, - reticulum_behavior: None, - }]); + request.target_policy = TargetPolicy::explicit_targets(vec![Target { + transport_kind: removed_proxy_transport_kind_string(), + endpoint_uri: removed_execution_endpoint_uri(), + target_scope: None, + target_label: None, + reticulum_behavior: None, + }]); let err = proxy .publish_event(&principal, request) @@ -5947,12 +5772,12 @@ mod tests { #[tokio::test] async fn publish_event_records_unsafe_request_relay_rejection() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(TransportPublishConfig::default()); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::ExplicitOnly], + vec![NostrTargetSourcePolicy::ExplicitOnly], true, PublishJobVisibility::Own, ); @@ -5962,22 +5787,19 @@ mod tests { publish_request( signed_event(&identity, "{}"), vec!["wss://127.0.0.1:7777".to_owned()], - NostrPublishTargetSourcePolicy::ExplicitOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::ExplicitOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliveryUnsatisfiedTerminal - ); + assert_eq!(response.job.status, JobStatus::DeliveryUnsatisfiedTerminal); assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::TargetRejected + OutcomeKind::TargetRejected ); assert!(!response.job.targets[0].attempted); assert!(adapter.captured_raw_events().is_empty()); @@ -5985,7 +5807,7 @@ mod tests { #[tokio::test] async fn publish_event_rejects_forbidden_public_dns_destination_before_publish() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let resolver = StaticPublishRelayResolver::new() .with_addresses(RELAY_PRIMARY, vec![IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))]); let (proxy, adapter) = transport_publish_with_resolver( @@ -5995,7 +5817,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6005,22 +5827,19 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliveryUnsatisfiedTerminal - ); + assert_eq!(response.job.status, JobStatus::DeliveryUnsatisfiedTerminal); assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::TargetRejected + OutcomeKind::TargetRejected ); assert!(!response.job.targets[0].attempted); assert!(adapter.captured_raw_events().is_empty()); @@ -6028,7 +5847,7 @@ mod tests { #[tokio::test] async fn publish_event_records_dns_failure_as_unattempted_retryable_outcome() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let resolver = StaticPublishRelayResolver::new().with_failure(RELAY_PRIMARY, "no records"); let (proxy, adapter) = transport_publish_with_resolver( config_with_defaults(vec![RELAY_PRIMARY]), @@ -6037,7 +5856,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6047,18 +5866,15 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - ); + assert_eq!(response.job.status, JobStatus::DeliveryUnsatisfiedRetryable); assert_eq!( response.job.last_error.as_deref(), Some("delivery_unsatisfied") @@ -6066,7 +5882,7 @@ mod tests { assert_eq!(response.job.targets.len(), 1); assert_eq!( response.job.targets[0].outcome_kind, - TransportPublishOutcomeKind::ConnectionFailed + OutcomeKind::ConnectionFailed ); assert!(!response.job.targets[0].attempted); assert!(adapter.captured_raw_events().is_empty()); @@ -6074,7 +5890,7 @@ mod tests { #[tokio::test] async fn publish_event_localhost_policy_skips_public_dns_guard() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let mut config = config_with_defaults(vec!["ws://localhost:7777"]); config.nostr.relay_url_policy = NostrRelayUrlPolicy::Localhost; let resolver = StaticPublishRelayResolver::new() @@ -6083,7 +5899,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6093,38 +5909,35 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliverySatisfied - ); + assert_eq!(response.job.status, JobStatus::DeliverySatisfied); assert_eq!(response.job.targets[0].endpoint_uri, "ws://localhost:7777"); assert!(!adapter.captured_raw_events().is_empty()); } #[tokio::test] async fn publish_event_deduplicates_same_intent_and_conflicts_different_intent() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); let request = publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-conflict"), ); let first = proxy @@ -6146,8 +5959,8 @@ mod tests { publish_request( signed_event(&identity, "changed"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-conflict"), ), ) @@ -6161,20 +5974,20 @@ mod tests { #[tokio::test] async fn publish_event_rejects_zero_and_excessive_timeout_before_job_creation() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); let mut zero = publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-zero-timeout"), ); zero.timeout_ms = Some(0); @@ -6190,8 +6003,8 @@ mod tests { let mut excessive = publish_request( signed_event(&identity, "changed"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-excessive-timeout"), ); excessive.timeout_ms = Some(10_001); @@ -6215,12 +6028,12 @@ mod tests { #[tokio::test] async fn publish_event_default_timeout_fingerprints_as_effective_timeout() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6228,16 +6041,16 @@ mod tests { let mut default_timeout = publish_request( event.clone(), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-default-timeout"), ); default_timeout.timeout_ms = None; let mut explicit_default = publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-default-timeout"), ); explicit_default.timeout_ms = Some(10_000); @@ -6257,12 +6070,12 @@ mod tests { #[tokio::test] async fn publish_event_fingerprint_conflicts_on_different_effective_timeout() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6270,15 +6083,15 @@ mod tests { let first = publish_request( event.clone(), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-timeout-conflict"), ); let mut conflict = publish_request( event, Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-timeout-conflict"), ); conflict.timeout_ms = Some(6_000); @@ -6296,14 +6109,14 @@ mod tests { #[tokio::test] async fn publish_event_concurrency_limit_rejects_without_job_creation() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let mut config = config_with_defaults(vec![RELAY_PRIMARY]); config.max_concurrent_publish_jobs = 1; let (proxy, adapter) = transport_publish(config); let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6314,8 +6127,8 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, Some("idem-concurrency"), ), ) @@ -6334,27 +6147,27 @@ mod tests { #[tokio::test] async fn publish_jobs_respect_own_and_admin_visibility() { - let identity = RadrootsIdentity::generate(); - let other_identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); + let other_identity = DaemonIdentity::generate(); let (proxy, _adapter) = transport_publish(config_with_defaults(vec![RELAY_PRIMARY])); let owner = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); let other = principal( &proxy, other_identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); let admin = principal( &proxy, other_identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Admin, ); @@ -6364,8 +6177,8 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) @@ -6390,7 +6203,7 @@ mod tests { #[tokio::test] async fn publish_event_records_retryable_relay_failures() { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let adapter = RadrootsMockRelayPublishAdapter::new().with_outcome( RELAY_PRIMARY, RadrootsRelayOutcome::connection_failed("error: unavailable"), @@ -6401,7 +6214,7 @@ mod tests { let principal = principal( &proxy, identity.public_key_hex(), - vec![NostrPublishTargetSourcePolicy::DaemonDefaultOnly], + vec![NostrTargetSourcePolicy::DaemonDefaultOnly], false, PublishJobVisibility::Own, ); @@ -6411,18 +6224,15 @@ mod tests { publish_request( signed_event(&identity, "{}"), Vec::new(), - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - TransportPublishDeliveryPolicy::Any, + NostrTargetSourcePolicy::DaemonDefaultOnly, + DeliveryPolicy::Any, None, ), ) .await .expect("publish"); - assert_eq!( - response.job.status, - TransportPublishJobStatus::DeliveryUnsatisfiedRetryable - ); + assert_eq!(response.job.status, JobStatus::DeliveryUnsatisfiedRetryable); assert_eq!(response.job.retryable_count, 1); } } diff --git a/src/host_nostr.rs b/src/host_nostr.rs @@ -0,0 +1,107 @@ +//! Daemon-owned Nostr client adapter. +//! +//! Radroots protocol crates deliberately do not own relay clients, process +//! lifecycle, or service keys. This private adapter keeps those host concerns +//! in `radrootsd` while using upstream protocol values at the explicit edge. + +use core::time::Duration; + +pub(crate) use nostr::{ + Event, Filter, Keys, Kind, Metadata, PublicKey, SecretKey, SubscriptionId, Timestamp, +}; +pub(crate) use nostr_sdk::RelayPoolNotification; +pub(crate) use radroots_nostr::event::{ + ApplicationHandlerSpec, build_application_handler, build_profile, +}; + +#[derive(Clone)] +pub(crate) struct DaemonNostrClient { + inner: nostr_sdk::Client, +} + +impl DaemonNostrClient { + pub(crate) fn signerless() -> Self { + let inner = nostr_sdk::Client::default(); + inner.automatic_authentication(false); + Self { inner } + } + + pub(crate) fn with_keys(keys: Keys) -> Self { + let inner = nostr_sdk::Client::new(keys); + inner.automatic_authentication(false); + Self { inner } + } + + #[cfg(test)] + pub(crate) fn from_identity(identity: &crate::app::identity_storage::DaemonIdentity) -> Self { + Self::with_keys(identity.keys().clone()) + } + + pub(crate) fn into_inner(self) -> nostr_sdk::Client { + self.inner + } + + pub(crate) async fn connect(&self) { + self.inner.connect().await; + } + + pub(crate) async fn wait_for_connection(&self, timeout: Duration) { + self.inner.wait_for_connection(timeout).await; + } + + pub(crate) async fn add_relay(&self, url: &str) -> Result<bool, nostr_sdk::client::Error> { + self.inner.add_relay(url).await + } + + pub(crate) async fn add_read_relay(&self, url: &str) -> Result<bool, nostr_sdk::client::Error> { + self.inner.add_read_relay(url).await + } + + pub(crate) async fn relays( + &self, + ) -> std::collections::HashMap<nostr::RelayUrl, nostr_sdk::Relay> { + self.inner.relays().await + } + + pub(crate) async fn fetch_events( + &self, + filter: Filter, + timeout: Duration, + ) -> Result<Vec<Event>, nostr_sdk::client::Error> { + self.inner + .fetch_events(filter, timeout) + .await + .map(|events| events.to_vec()) + } + + pub(crate) async fn subscribe( + &self, + filter: Filter, + options: Option<nostr_sdk::SubscribeAutoCloseOptions>, + ) -> Result<nostr_sdk::prelude::Output<SubscriptionId>, nostr_sdk::client::Error> { + self.inner.subscribe(filter, options).await + } + + pub(crate) async fn unsubscribe(&self, subscription_id: &SubscriptionId) { + self.inner.unsubscribe(subscription_id).await; + } + + pub(crate) async fn send_event( + &self, + event: &Event, + ) -> Result<nostr_sdk::prelude::Output<nostr::EventId>, nostr_sdk::client::Error> { + self.inner.send_event(event).await + } +} + +pub(crate) fn parse_public_key(value: &str) -> Result<PublicKey, radroots_nostr::Error> { + radroots_nostr::key::parse_public_key(value).and_then(radroots_nostr::key::public_key_to_nostr) +} + +pub(crate) fn with_filter_tag( + filter: Filter, + tag: &str, + values: Vec<String>, +) -> Result<Filter, radroots_nostr::Error> { + radroots_nostr::filter::with_tag(filter, tag, values) +} diff --git a/src/lib.rs b/src/lib.rs @@ -3,6 +3,7 @@ pub mod app; pub mod core; +mod host_nostr; #[cfg_attr(coverage_nightly, coverage(off))] pub mod transport; diff --git a/src/transport/jsonrpc/auth.rs b/src/transport/jsonrpc/auth.rs @@ -59,8 +59,8 @@ pub(crate) fn require_publish_principal( #[cfg(test)] mod tests { use jsonrpsee::core::server::Extensions; - use radroots_transport_publish_protocol::{ - NostrPublishTargetSourcePolicy, TransportPublishTargetPolicyName, + use radroots_protocol::radrootsd::transport_publish::v5::{ + NostrTargetSourcePolicy, TargetPolicyName, }; use super::{ @@ -81,11 +81,9 @@ mod tests { token_hash: hash_bearer_token(token.as_str()), allowed_pubkeys: vec!["a".repeat(64)], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, diff --git a/src/transport/jsonrpc/methods/mod.rs b/src/transport/jsonrpc/methods/mod.rs @@ -24,10 +24,10 @@ pub fn register_all( #[cfg(test)] mod tests { + use crate::app::identity_storage::DaemonIdentity; + use crate::host_nostr::Metadata; use jsonrpsee::server::RpcModule; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::RadrootsNostrMetadata; - use radroots_transport::RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE; + use radroots_protocol::radrootsd::transport_publish::v5::RETICULUM_UNAVAILABLE_MESSAGE as RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE; use super::register_all; use crate::app::config::{Nip46Config, TransportPublishConfig}; @@ -40,8 +40,8 @@ mod tests { } fn state(transport_publish_enabled: bool, nip46_public_jsonrpc_enabled: bool) -> Radrootsd { - let identity = RadrootsIdentity::generate(); - let metadata: RadrootsNostrMetadata = + let identity = DaemonIdentity::generate(); + let metadata: Metadata = serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata"); let transport_publish = TransportPublishConfig { enabled: transport_publish_enabled, @@ -114,11 +114,11 @@ mod tests { allowed_pubkeys: vec!["a".repeat(64)], allowed_kinds: vec![30_402], allowed_target_policies: vec![ - radroots_transport_publish_protocol::TransportPublishTargetPolicyName::Nostr, + radroots_protocol::radrootsd::transport_publish::v5::TargetPolicyName::Nostr, ], allowed_explicit_transport_kinds: Vec::new(), allowed_nostr_source_policies: vec![ - radroots_transport_publish_protocol::NostrPublishTargetSourcePolicy::DaemonDefaultOnly, + radroots_protocol::radrootsd::transport_publish::v5::NostrTargetSourcePolicy::DaemonDefaultOnly, ], allow_request_targets: false, job_visibility: crate::core::transport_publish::PublishJobVisibility::Own, diff --git a/src/transport/jsonrpc/methods/nip46/connect.rs b/src/transport/jsonrpc/methods/nip46/connect.rs @@ -10,6 +10,10 @@ use uuid::Uuid; use crate::core::nip46::session::{ Nip46Session, Nip46SessionAuthority, filter_perms, session_expires_at, }; +use crate::host_nostr::{ + DaemonNostrClient, Filter, Keys, Kind, PublicKey, RelayPoolNotification, SecretKey, + SubscriptionId, Timestamp, parse_public_key, with_filter_tag, +}; use crate::transport::jsonrpc::nip46::connection::{ Nip46ConnectInfo, Nip46ConnectMode, parse_connect_url, }; @@ -18,12 +22,6 @@ use crate::transport::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use crate::transport::nostr::protocol::sign_nip46_message; use nostr::JsonUtil; use nostr::nips::{nip44, nip46::NostrConnectMessage, nip46::NostrConnectRequest}; -use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrFilter, RadrootsNostrKeys, RadrootsNostrKind, - RadrootsNostrPublicKey, RadrootsNostrRelayPoolNotification, RadrootsNostrSecretKey, - RadrootsNostrSubscriptionId, RadrootsNostrTimestamp, radroots_nostr_filter_tag, - radroots_nostr_parse_pubkey, -}; #[derive(Debug, Deserialize)] struct Nip46ConnectParams { @@ -94,12 +92,12 @@ async fn connect_bunker( .remote_signer_pubkey .as_ref() .ok_or_else(|| RpcError::InvalidParams("missing remote signer pubkey".to_string()))?; - let remote_signer_pubkey = radroots_nostr_parse_pubkey(remote_signer_raw) + let remote_signer_pubkey = parse_public_key(remote_signer_raw) .map_err(|e| RpcError::InvalidParams(format!("invalid remote signer: {e}")))?; - let client_keys = RadrootsNostrKeys::generate(); + let client_keys = Keys::generate(); let client_pubkey = client_keys.public_key(); - let client = RadrootsNostrClient::new(client_keys.clone()); + let client = DaemonNostrClient::with_keys(client_keys.clone()); add_relays(&client, &info.relays).await?; client.connect().await; @@ -113,11 +111,7 @@ async fn connect_bunker( }; let message = NostrConnectMessage::request(&request); let request_id = message.id().to_string(); - let filter = connect_response_filter( - &remote_signer_pubkey, - &client_pubkey, - RadrootsNostrTimestamp::now(), - )?; + let filter = connect_response_filter(&remote_signer_pubkey, &client_pubkey, Timestamp::now())?; let notifications = client.clone().into_inner().notifications(); let subscription = client .subscribe(filter, None) @@ -195,15 +189,15 @@ async fn connect_nostrconnect( .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| RpcError::InvalidParams("missing client_secret_key".to_string()))?; - let client_secret_key = RadrootsNostrSecretKey::parse(&client_secret_key) + let client_secret_key = SecretKey::parse(&client_secret_key) .map_err(|e| RpcError::InvalidParams(format!("invalid client_secret_key: {e}")))?; - let client_keys = RadrootsNostrKeys::new(client_secret_key); + let client_keys = Keys::new(client_secret_key); let client_pubkey = client_keys.public_key(); let client_pubkey_raw = info .client_pubkey .as_ref() .ok_or_else(|| RpcError::InvalidParams("missing client pubkey".to_string()))?; - let expected_pubkey = radroots_nostr_parse_pubkey(client_pubkey_raw) + let expected_pubkey = parse_public_key(client_pubkey_raw) .map_err(|e| RpcError::InvalidParams(format!("invalid client pubkey: {e}")))?; if expected_pubkey != client_pubkey { return Err(RpcError::InvalidParams( @@ -211,7 +205,7 @@ async fn connect_nostrconnect( )); } - let client = RadrootsNostrClient::new(client_keys.clone()); + let client = DaemonNostrClient::with_keys(client_keys.clone()); add_relays(&client, &info.relays).await?; client.connect().await; client @@ -257,7 +251,7 @@ async fn connect_nostrconnect( }) } -async fn add_relays(client: &RadrootsNostrClient, relays: &[String]) -> Result<(), RpcError> { +async fn add_relays(client: &DaemonNostrClient, relays: &[String]) -> Result<(), RpcError> { for relay in relays.iter() { client .add_relay(relay) @@ -283,9 +277,9 @@ async fn claim_secret(ctx: &RpcContext, secret: Option<&str>) -> Result<(), RpcE } async fn send_connect_request( - client: &RadrootsNostrClient, - client_keys: &RadrootsNostrKeys, - remote_signer_pubkey: &RadrootsNostrPublicKey, + client: &DaemonNostrClient, + client_keys: &Keys, + remote_signer_pubkey: &PublicKey, message: NostrConnectMessage, ) -> Result<(), RpcError> { let event = sign_nip46_message(client_keys, *remote_signer_pubkey, message) @@ -298,25 +292,25 @@ async fn send_connect_request( } fn connect_response_filter( - remote_signer_pubkey: &RadrootsNostrPublicKey, - client_pubkey: &RadrootsNostrPublicKey, - since: RadrootsNostrTimestamp, -) -> Result<RadrootsNostrFilter, RpcError> { - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::NostrConnect) + remote_signer_pubkey: &PublicKey, + client_pubkey: &PublicKey, + since: Timestamp, +) -> Result<Filter, RpcError> { + let filter = Filter::new() + .kind(Kind::NostrConnect) .author(*remote_signer_pubkey) .since(since); - radroots_nostr_filter_tag(filter, "p", vec![client_pubkey.to_hex()]) + with_filter_tag(filter, "p", vec![client_pubkey.to_hex()]) .map_err(|e| RpcError::Other(format!("nip46 connect filter failed: {e}"))) } async fn wait_for_connect_response( - client: &RadrootsNostrClient, - client_keys: &RadrootsNostrKeys, - remote_signer_pubkey: &RadrootsNostrPublicKey, + client: &DaemonNostrClient, + client_keys: &Keys, + remote_signer_pubkey: &PublicKey, request_id: &str, - mut notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>, - subscription_id: &RadrootsNostrSubscriptionId, + mut notifications: broadcast::Receiver<RelayPoolNotification>, + subscription_id: &SubscriptionId, ) -> Result<NostrConnectMessage, RpcError> { let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS)); tokio::pin!(timeout); @@ -336,11 +330,11 @@ async fn wait_for_connect_response( return Err(RpcError::Other("nip46 connect notification closed".to_string())); } }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { + let RelayPoolNotification::Event { event, .. } = notification else { continue; }; let event = (*event).clone(); - if event.kind != RadrootsNostrKind::NostrConnect + if event.kind != Kind::NostrConnect || event.pubkey != *remote_signer_pubkey { continue; @@ -427,15 +421,15 @@ fn validate_nostrconnect_response( } async fn wait_for_nostrconnect_response( - client: &RadrootsNostrClient, - client_keys: &RadrootsNostrKeys, - client_pubkey: &RadrootsNostrPublicKey, + client: &DaemonNostrClient, + client_keys: &Keys, + client_pubkey: &PublicKey, secret: &str, -) -> Result<(RadrootsNostrPublicKey, NostrConnectMessage), RpcError> { - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::NostrConnect) - .since(RadrootsNostrTimestamp::now()); - let filter = radroots_nostr_filter_tag(filter, "p", vec![client_pubkey.to_hex()]) +) -> Result<(PublicKey, NostrConnectMessage), RpcError> { + let filter = Filter::new() + .kind(Kind::NostrConnect) + .since(Timestamp::now()); + let filter = with_filter_tag(filter, "p", vec![client_pubkey.to_hex()]) .map_err(|e| RpcError::Other(format!("nip46 connect filter failed: {e}")))?; let mut notifications = client.clone().into_inner().notifications(); let subscription = client @@ -459,11 +453,11 @@ async fn wait_for_nostrconnect_response( return Err(RpcError::Other("nip46 connect notification closed".to_string())); } }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { + let RelayPoolNotification::Event { event, .. } = notification else { continue; }; let event = (*event).clone(); - if event.kind != RadrootsNostrKind::NostrConnect { + if event.kind != Kind::NostrConnect { continue; } let decrypted = nip44::decrypt( diff --git a/src/transport/jsonrpc/methods/nip46/get_public_key.rs b/src/transport/jsonrpc/methods/nip46/get_public_key.rs @@ -43,7 +43,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res async fn request_get_public_key( session: &Nip46Session, -) -> Result<(radroots_nostr::prelude::RadrootsNostrPublicKey, bool), RpcError> { +) -> Result<(crate::host_nostr::PublicKey, bool), RpcError> { let req = NostrConnectRequest::GetPublicKey; let response = client::request(session, req, "get_public_key").await?; let response = response diff --git a/src/transport/jsonrpc/methods/nip46/nip04.rs b/src/transport/jsonrpc/methods/nip46/nip04.rs @@ -42,7 +42,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let session = session::get_session(ctx.as_ref(), &session_id).await?; session::require_permission(&session, "nip04_encrypt")?; - let public_key = radroots_nostr::prelude::radroots_nostr_parse_pubkey(&public_key) + let public_key = crate::host_nostr::parse_public_key(&public_key) .map_err(|e| RpcError::InvalidParams(format!("invalid public_key: {e}")))?; let req = NostrConnectRequest::Nip04Encrypt { public_key, text }; let response = client::request(&session, req, "nip04_encrypt").await?; @@ -81,7 +81,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let session = session::get_session(ctx.as_ref(), &session_id).await?; session::require_permission(&session, "nip04_decrypt")?; - let public_key = radroots_nostr::prelude::radroots_nostr_parse_pubkey(&public_key) + let public_key = crate::host_nostr::parse_public_key(&public_key) .map_err(|e| RpcError::InvalidParams(format!("invalid public_key: {e}")))?; let req = NostrConnectRequest::Nip04Decrypt { public_key, diff --git a/src/transport/jsonrpc/methods/nip46/nip44.rs b/src/transport/jsonrpc/methods/nip46/nip44.rs @@ -42,7 +42,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let session = session::get_session(ctx.as_ref(), &session_id).await?; session::require_permission(&session, "nip44_encrypt")?; - let public_key = radroots_nostr::prelude::radroots_nostr_parse_pubkey(&public_key) + let public_key = crate::host_nostr::parse_public_key(&public_key) .map_err(|e| RpcError::InvalidParams(format!("invalid public_key: {e}")))?; let req = NostrConnectRequest::Nip44Encrypt { public_key, text }; let response = client::request(&session, req, "nip44_encrypt").await?; @@ -81,7 +81,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let session = session::get_session(ctx.as_ref(), &session_id).await?; session::require_permission(&session, "nip44_decrypt")?; - let public_key = radroots_nostr::prelude::radroots_nostr_parse_pubkey(&public_key) + let public_key = crate::host_nostr::parse_public_key(&public_key) .map_err(|e| RpcError::InvalidParams(format!("invalid public_key: {e}")))?; let req = NostrConnectRequest::Nip44Decrypt { public_key, diff --git a/src/transport/jsonrpc/methods/transport_publish.rs b/src/transport/jsonrpc/methods/transport_publish.rs @@ -1,8 +1,7 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; -use radroots_transport_publish_protocol::{ - METHOD_CAPABILITIES, METHOD_EVENT, METHOD_JOB_GET, METHOD_JOB_LIST, - TransportPublishCapabilities, TransportPublishEventRequest, +use radroots_protocol::radrootsd::transport_publish::v5::{ + Capabilities, EventRequest, METHOD_CAPABILITIES, METHOD_EVENT, METHOD_JOB_GET, METHOD_JOB_LIST, }; use serde::Deserialize; @@ -37,7 +36,7 @@ fn register_capabilities( registry.track(METHOD_CAPABILITIES); module.register_async_method(METHOD_CAPABILITIES, |_params, ctx, extensions| async move { require_publish_principal(&extensions)?; - Ok::<TransportPublishCapabilities, RpcError>(TransportPublishCapabilities::v5( + Ok::<Capabilities, RpcError>(Capabilities::v5( ctx.state.transport_publish.config.max_event_bytes, ctx.state.transport_publish.config.max_targets_per_request, )) @@ -49,7 +48,7 @@ fn register_event(module: &mut RpcModule<RpcContext>, registry: &MethodRegistry) registry.track(METHOD_EVENT); module.register_async_method(METHOD_EVENT, |params, ctx, extensions| async move { let principal = require_publish_principal(&extensions)?; - let request: TransportPublishEventRequest = params + let request: EventRequest = params .parse() .map_err(|error| RpcError::InvalidParams(error.to_string()))?; ctx.state @@ -118,7 +117,6 @@ fn rpc_error_from_transport_publish(error: TransportPublishError) -> RpcError { TransportPublishError::InvalidSignedEvent(message) => RpcError::InvalidParams(message), TransportPublishError::EventWire(_) | TransportPublishError::SignedEvent(_) - | TransportPublishError::SignedEventSignature(_) | TransportPublishError::Relay(_) => RpcError::InvalidParams(error.to_string()), TransportPublishError::IdempotencyConflict(_) => RpcError::Other(error.to_string()), other => RpcError::Other(other.to_string()), @@ -131,30 +129,30 @@ mod tests { use std::sync::Arc; use crate::app::config::{Nip46Config, TransportPublishConfig, TransportPublishNostrConfig}; + use crate::app::identity_storage::DaemonIdentity; use crate::core::Radrootsd; use crate::core::transport_publish::{ PublishJobVisibility, PublishPrincipalInit, generate_bearer_token, hash_bearer_token, }; + use crate::host_nostr::{Metadata, Timestamp}; use crate::transport::jsonrpc::auth::{ TransportPublishAuthorization, authorize_transport_publish_request, }; use crate::transport::jsonrpc::{MethodRegistry, RpcContext}; + use crate::transport::relay_publish::MockRelayPublishAdapter as RadrootsMockRelayPublishAdapter; use jsonrpsee::server::RpcModule; use nostr::JsonUtil; use nostr::{EventBuilder, Kind, Tag}; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::{RadrootsNostrMetadata, RadrootsNostrTimestamp}; - use radroots_transport_nostr::RadrootsMockRelayPublishAdapter; - use radroots_transport_publish_protocol::{ - NostrPublishTargetSourcePolicy, TransportPublishTargetPolicyName, + use radroots_protocol::radrootsd::transport_publish::v5::{ + NostrTargetSourcePolicy, TargetPolicyName, }; - fn signed_event(identity: &RadrootsIdentity) -> String { + fn signed_event(identity: &DaemonIdentity) -> String { // This method accepts an already-signed wire event; construct the test // fixture at that explicit low-level interoperability boundary. let event = EventBuilder::new(Kind::Custom(30_402), "{}") .tag(Tag::identifier("listing-1")) - .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000)) + .custom_created_at(Timestamp::from_secs(1_700_000_000)) .sign_with_keys(identity.keys()) .expect("signed event"); event.as_json() @@ -164,9 +162,9 @@ mod tests { admin: bool, transport_publish_config: TransportPublishConfig, ) -> (RpcModule<RpcContext>, RpcContext, String, String) { - let identity = RadrootsIdentity::generate(); + let identity = DaemonIdentity::generate(); let signed_event = signed_event(&identity); - let metadata: RadrootsNostrMetadata = + let metadata: Metadata = serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata"); let state = Radrootsd::new( identity.clone(), @@ -189,11 +187,9 @@ mod tests { token_hash: hash_bearer_token(token.as_str()), allowed_pubkeys: vec![identity.public_key_hex()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: if admin { PublishJobVisibility::Admin @@ -257,7 +253,7 @@ mod tests { #[tokio::test] async fn publish_event_rejects_principal_scope_gap() { let (module, _ctx, _token, _pubkey) = module_with_principal(false); - let other_identity = RadrootsIdentity::generate(); + let other_identity = DaemonIdentity::generate(); let event = signed_event(&other_identity); let request = format!( r#"{{ diff --git a/src/transport/jsonrpc/nip46/client.rs b/src/transport/jsonrpc/nip46/client.rs @@ -3,6 +3,9 @@ use std::time::Duration; use crate::core::nip46::session::Nip46Session; +use crate::host_nostr::{ + Filter, Kind, RelayPoolNotification, SubscriptionId, Timestamp, with_filter_tag, +}; use crate::transport::jsonrpc::{RpcError, params::DEFAULT_TIMEOUT_SECS}; use crate::transport::nostr::protocol::sign_nip46_message; use nostr::JsonUtil; @@ -11,10 +14,6 @@ use nostr::nips::{ nip44, nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult}, }; -use radroots_nostr::prelude::{ - RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrRelayPoolNotification, - RadrootsNostrSubscriptionId, RadrootsNostrTimestamp, radroots_nostr_filter_tag, -}; use tokio::sync::broadcast; use tokio::time::sleep; @@ -64,7 +63,7 @@ pub async fn request( let message = NostrConnectMessage::request(&request); let request_id = message.id().to_string(); - let filter = response_filter(session, RadrootsNostrTimestamp::now(), label)?; + let filter = response_filter(session, Timestamp::now(), label)?; let notifications = session.client.clone().into_inner().notifications(); let subscription = session .client @@ -118,14 +117,14 @@ fn validate_signed_event_response( fn response_filter( session: &Nip46Session, - since: RadrootsNostrTimestamp, + since: Timestamp, label: &str, -) -> Result<RadrootsNostrFilter, RpcError> { - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::NostrConnect) +) -> Result<Filter, RpcError> { + let filter = Filter::new() + .kind(Kind::NostrConnect) .author(session.remote_signer_pubkey) .since(since); - radroots_nostr_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()]) + with_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()]) .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}"))) } @@ -133,8 +132,8 @@ async fn wait_for_response( session: &Nip46Session, request_id: &str, label: &str, - mut notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>, - subscription_id: &RadrootsNostrSubscriptionId, + mut notifications: broadcast::Receiver<RelayPoolNotification>, + subscription_id: &SubscriptionId, ) -> Result<NostrConnectMessage, RpcError> { let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS)); tokio::pin!(timeout); @@ -154,11 +153,11 @@ async fn wait_for_response( return Err(RpcError::Other(format!("nip46 {label} notification closed"))); } }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { + let RelayPoolNotification::Event { event, .. } = notification else { continue; }; let event = (*event).clone(); - if event.kind != RadrootsNostrKind::NostrConnect + if event.kind != Kind::NostrConnect || event.pubkey != session.remote_signer_pubkey { continue; @@ -182,13 +181,13 @@ async fn wait_for_response( #[cfg(test)] mod tests { + use crate::host_nostr::Keys; use nostr::{EventBuilder, EventId, Kind, Timestamp}; - use radroots_nostr::prelude::RadrootsNostrKeys; use super::validate_signed_event_response; fn signed_fixture() -> (nostr::UnsignedEvent, nostr::Event) { - let keys = RadrootsNostrKeys::generate(); + let keys = Keys::generate(); let unsigned = EventBuilder::new(Kind::Custom(30_001), "checked") .custom_created_at(Timestamp::from_secs(1_784_347_200)) .build(keys.public_key()); diff --git a/src/transport/jsonrpc/nip46/connection.rs b/src/transport/jsonrpc/nip46/connection.rs @@ -1,4 +1,4 @@ -use radroots_nostr::prelude::radroots_nostr_parse_pubkey; +use crate::host_nostr::parse_public_key; use serde::{Deserialize, Serialize}; use url::Url; @@ -74,7 +74,7 @@ fn parse_nostrconnect_url(url: &Url) -> Result<Nip46ConnectInfo, RpcError> { .host_str() .map(|host| host.to_string()) .ok_or_else(|| RpcError::InvalidParams("missing client pubkey".to_string()))?; - radroots_nostr_parse_pubkey(&client_pubkey) + parse_public_key(&client_pubkey) .map_err(|e| RpcError::InvalidParams(format!("invalid client pubkey: {e}")))?; let query: Nip46ConnectQuery = serde_qs::from_str(url.query().unwrap_or_default()) .map_err(|e| RpcError::InvalidParams(e.to_string()))?; diff --git a/src/transport/jsonrpc/server.rs b/src/transport/jsonrpc/server.rs @@ -125,21 +125,21 @@ mod tests { Nip46Config, NostrRelayUrlPolicy, RpcConfig, TransportPublishConfig, TransportPublishNostrConfig, }; + use crate::app::identity_storage::DaemonIdentity; use crate::core::Radrootsd; use crate::core::transport_publish::{ PublishJobVisibility, PublishPrincipalInit, PublishRelayResolveFuture, PublishRelayResolver, generate_bearer_token, hash_bearer_token, }; + use crate::host_nostr::{Metadata, Timestamp}; use crate::transport::jsonrpc::methods; use crate::transport::jsonrpc::{MethodRegistry, RpcContext}; + use crate::transport::relay_publish::MockRelayPublishAdapter as RadrootsMockRelayPublishAdapter; use jsonrpsee::server::RpcModule; use nostr::JsonUtil; use nostr::{EventBuilder, Kind, Tag}; - use radroots_identity::RadrootsIdentity; - use radroots_nostr::prelude::{RadrootsNostrMetadata, RadrootsNostrTimestamp}; - use radroots_transport_nostr::RadrootsMockRelayPublishAdapter; - use radroots_transport_publish_protocol::{ - NostrPublishTargetSourcePolicy, TransportPublishTargetPolicyName, + use radroots_protocol::radrootsd::transport_publish::v5::{ + NostrTargetSourcePolicy, TargetPolicyName, }; use serde_json::Value; use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; @@ -154,11 +154,11 @@ mod tests { listener.local_addr().expect("local addr") } - fn signed_event_json(identity: &RadrootsIdentity) -> String { + fn signed_event_json(identity: &DaemonIdentity) -> String { // The JSON-RPC server consumes an already-signed transport fixture. EventBuilder::new(Kind::Custom(30_402), "{}") .tag(Tag::identifier("listing-1")) - .custom_created_at(RadrootsNostrTimestamp::from_secs(1_700_000_000)) + .custom_created_at(Timestamp::from_secs(1_700_000_000)) .sign_with_keys(identity.keys()) .expect("signed event") .as_json() @@ -188,11 +188,11 @@ mod tests { ) -> ( Radrootsd, String, - RadrootsIdentity, + DaemonIdentity, RadrootsMockRelayPublishAdapter, ) { - let identity = RadrootsIdentity::generate(); - let metadata: RadrootsNostrMetadata = + let identity = DaemonIdentity::generate(); + let metadata: Metadata = serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata"); let mut state = Radrootsd::new( identity.clone(), @@ -216,11 +216,9 @@ mod tests { token_hash: hash_bearer_token(token.as_str()), allowed_pubkeys: vec![identity.public_key_hex()], allowed_kinds: vec![30_402], - allowed_target_policies: vec![TransportPublishTargetPolicyName::Nostr], + allowed_target_policies: vec![TargetPolicyName::Nostr], allowed_explicit_transport_kinds: Vec::new(), - allowed_nostr_source_policies: vec![ - NostrPublishTargetSourcePolicy::DaemonDefaultOnly, - ], + allowed_nostr_source_policies: vec![NostrTargetSourcePolicy::DaemonDefaultOnly], allow_request_targets: false, job_visibility: PublishJobVisibility::Own, expires_at_unix: None, @@ -232,7 +230,7 @@ mod tests { fn publish_server_state() -> ( Radrootsd, String, - RadrootsIdentity, + DaemonIdentity, RadrootsMockRelayPublishAdapter, ) { publish_server_state_with_config( @@ -263,7 +261,7 @@ mod tests { impl PublishRelayResolver for StaticPublishRelayResolver { fn resolve<'a>( &'a self, - _url: &'a radroots_transport_nostr::RadrootsRelayUrl, + _url: &'a crate::transport::relay_publish::RelayUrl, ) -> PublishRelayResolveFuture<'a> { Box::pin(async move { Ok(self.addresses.clone()) }) } diff --git a/src/transport/mod.rs b/src/transport/mod.rs @@ -1,2 +1,3 @@ pub mod jsonrpc; pub mod nostr; +pub(crate) mod relay_publish; diff --git a/src/transport/nostr/listener.rs b/src/transport/nostr/listener.rs @@ -14,11 +14,8 @@ use crate::core::nip46::session::{ Nip46Session, PendingNostrRequest, session_expires_at, sign_event_allowed, }; use crate::core::state::Radrootsd; +use crate::host_nostr::{Filter, Kind, RelayPoolNotification, Timestamp, with_filter_tag}; use crate::transport::nostr::protocol::sign_nip46_message; -use radroots_nostr::prelude::{ - RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrRelayPoolNotification, - RadrootsNostrTimestamp, radroots_nostr_filter_tag, -}; const DEFAULT_TIMEOUT_SECS: u64 = 10; @@ -37,10 +34,10 @@ async fn run_nip46_listener(radrootsd: Radrootsd) -> Result<()> { .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) .await; - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::NostrConnect) - .since(RadrootsNostrTimestamp::now()); - let filter = radroots_nostr_filter_tag(filter, "p", vec![radrootsd.pubkey.to_hex()])?; + let filter = Filter::new() + .kind(Kind::NostrConnect) + .since(Timestamp::now()); + let filter = with_filter_tag(filter, "p", vec![radrootsd.pubkey.to_hex()])?; let mut notifications = radrootsd.client.clone().into_inner().notifications(); let subscription = radrootsd.client.subscribe(filter, None).await?; @@ -54,11 +51,11 @@ async fn run_nip46_listener(radrootsd: Radrootsd) -> Result<()> { return Err(anyhow!("nip46 listener notification closed")); } }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { + let RelayPoolNotification::Event { event, .. } = notification else { continue; }; let event = (*event).clone(); - if event.kind != RadrootsNostrKind::NostrConnect { + if event.kind != Kind::NostrConnect { continue; } @@ -99,7 +96,7 @@ async fn run_nip46_listener(radrootsd: Radrootsd) -> Result<()> { pub(crate) async fn handle_request( radrootsd: &Radrootsd, - client_pubkey: &radroots_nostr::prelude::RadrootsNostrPublicKey, + client_pubkey: &crate::host_nostr::PublicKey, request_id: &str, request: NostrConnectRequest, ) -> NostrConnectResponse { @@ -317,7 +314,7 @@ pub(crate) async fn handle_request( async fn session_for_client( radrootsd: &Radrootsd, - client_pubkey: &radroots_nostr::prelude::RadrootsNostrPublicKey, + client_pubkey: &crate::host_nostr::PublicKey, ) -> Result<Nip46Session, NostrConnectResponse> { let session_id = client_pubkey.to_hex(); match radrootsd.nip46_sessions.get(&session_id).await { @@ -338,7 +335,7 @@ async fn auth_challenge( radrootsd: &Radrootsd, session: &Nip46Session, request_id: &str, - client_pubkey: &radroots_nostr::prelude::RadrootsNostrPublicKey, + client_pubkey: &crate::host_nostr::PublicKey, request: NostrConnectRequest, ) -> Option<NostrConnectResponse> { if !session.auth_required || session.authorized { diff --git a/src/transport/nostr/protocol.rs b/src/transport/nostr/protocol.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] +use crate::host_nostr::{Keys, PublicKey}; use nostr::nips::nip46::NostrConnectMessage; use nostr::{Event, EventBuilder}; -use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrPublicKey}; /// Encrypts and locally signs a NIP-46 transport message. /// @@ -10,8 +10,8 @@ use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrPublicKey}; /// kind 24133 is protocol traffic, not a generic Radroots product-authoring /// surface. pub(crate) fn sign_nip46_message( - sender_keys: &RadrootsNostrKeys, - receiver_pubkey: RadrootsNostrPublicKey, + sender_keys: &Keys, + receiver_pubkey: PublicKey, message: NostrConnectMessage, ) -> Result<Event, nostr::event::builder::Error> { EventBuilder::nostr_connect(sender_keys, receiver_pubkey, message)?.sign_with_keys(sender_keys) @@ -19,23 +19,23 @@ pub(crate) fn sign_nip46_message( #[cfg(test)] mod tests { + use crate::host_nostr::{Keys, Kind}; use nostr::JsonUtil; use nostr::nips::{nip44, nip46::NostrConnectRequest}; - use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrKind}; use super::sign_nip46_message; #[test] fn signed_nip46_message_is_bound_to_sender_and_receiver() { - let sender = RadrootsNostrKeys::generate(); - let receiver = RadrootsNostrKeys::generate(); + let sender = Keys::generate(); + let receiver = Keys::generate(); let message = nostr::nips::nip46::NostrConnectMessage::request(&NostrConnectRequest::Ping); let request_id = message.id().to_owned(); let event = sign_nip46_message(&sender, receiver.public_key(), message) .expect("signed NIP-46 message"); - assert_eq!(event.kind, RadrootsNostrKind::NostrConnect); + assert_eq!(event.kind, Kind::NostrConnect); assert_eq!(event.pubkey, sender.public_key()); assert!( event diff --git a/src/transport/relay_publish.rs b/src/transport/relay_publish.rs @@ -0,0 +1,333 @@ +//! Daemon-owned adaptation from the V5 publish RPC to bounded Nostr attempts. + +use core::future::Future; +use core::pin::Pin; +use core::time::Duration; +#[cfg(test)] +use std::collections::BTreeMap; +use std::net::IpAddr; +#[cfg(test)] +use std::sync::{Arc, Mutex}; + +use nostr::JsonUtil; +use radroots_event::SignedEvent; +use radroots_transport::RadrootsTransportSatisfactionPolicy; + +use crate::host_nostr::DaemonNostrClient; + +pub(crate) type PublishFuture<'a> = + Pin<Box<dyn Future<Output = Result<Vec<RelayPublishReceipt>, RelayPublishError>> + Send + 'a>>; + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum RelayUrlPolicy { + Public, + Localhost, +} + +impl RelayUrlPolicy { + const fn native(self) -> radroots_transport_nostr::RelayUrlPolicy { + match self { + Self::Public => radroots_transport_nostr::RelayUrlPolicy::Public, + Self::Localhost => radroots_transport_nostr::RelayUrlPolicy::Local, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) struct RelayUrl { + inner: radroots_transport_nostr::RelayUrl, + policy: RelayUrlPolicy, +} + +impl RelayUrl { + pub(crate) fn parse( + value: impl AsRef<str>, + policy: RelayUrlPolicy, + ) -> Result<Self, RelayPublishError> { + let inner = radroots_transport_nostr::RelayUrl::parse(value, policy.native()) + .map_err(|error| RelayPublishError(error.to_string()))?; + Ok(Self { inner, policy }) + } + + pub(crate) fn as_str(&self) -> &str { + self.inner.as_str() + } + + pub(crate) fn validate_public_resolved_ip_addrs( + &self, + addresses: impl IntoIterator<Item = IpAddr>, + ) -> Result<(), RelayPublishError> { + self.inner + .validate_resolved_addresses(self.policy.native(), addresses) + .map_err(|error| RelayPublishError(error.to_string())) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct RelayTargetSet { + relays: Vec<RelayUrl>, +} + +impl RelayTargetSet { + pub(crate) fn from_urls(mut relays: Vec<RelayUrl>) -> Result<Self, RelayPublishError> { + if relays.is_empty() { + return Err(RelayPublishError( + "relay target set must not be empty".to_owned(), + )); + } + relays.sort(); + relays.dedup(); + Ok(Self { relays }) + } + + pub(crate) fn relays(&self) -> &[RelayUrl] { + self.relays.as_slice() + } +} + +#[derive(Clone, Debug)] +pub(crate) struct RelayPublishRequest { + signed_event: SignedEvent, + targets: RelayTargetSet, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + now_ms: i64, +} + +impl RelayPublishRequest { + pub(crate) fn new(signed_event: SignedEvent, targets: RelayTargetSet, now_ms: i64) -> Self { + Self { + signed_event, + targets, + satisfaction_policy: RadrootsTransportSatisfactionPolicy::all_accepted(), + now_ms, + } + } + + pub(crate) fn with_satisfaction_policy( + mut self, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + ) -> Self { + self.satisfaction_policy = satisfaction_policy; + self + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RelayOutcomeKind { + Accepted, + DuplicateAccepted, + Blocked, + RateLimited, + Invalid, + PowRequired, + Restricted, + AuthRequired, + Muted, + Unsupported, + PaymentRequired, + Error, + Timeout, + ConnectionFailed, + Unknown, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct RelayOutcome { + pub(crate) kind: RelayOutcomeKind, + pub(crate) message: Option<String>, +} + +impl RelayOutcome { + pub(crate) const fn accepted() -> Self { + Self { + kind: RelayOutcomeKind::Accepted, + message: None, + } + } + + pub(crate) fn classify(message: impl AsRef<str>) -> Self { + let message = message.as_ref().trim().to_owned(); + let lower = message.to_ascii_lowercase(); + let kind = if lower.starts_with("duplicate:") { + RelayOutcomeKind::DuplicateAccepted + } else if lower.starts_with("blocked:") { + RelayOutcomeKind::Blocked + } else if lower.starts_with("rate-limited:") || lower.contains("rate limit") { + RelayOutcomeKind::RateLimited + } else if lower.starts_with("invalid:") { + RelayOutcomeKind::Invalid + } else if lower.starts_with("pow:") { + RelayOutcomeKind::PowRequired + } else if lower.starts_with("restricted:") { + RelayOutcomeKind::Restricted + } else if lower.starts_with("auth-required:") || lower.contains("auth required") { + RelayOutcomeKind::AuthRequired + } else if lower.starts_with("mute:") { + RelayOutcomeKind::Muted + } else if lower.starts_with("unsupported:") { + RelayOutcomeKind::Unsupported + } else if lower.starts_with("payment-required:") { + RelayOutcomeKind::PaymentRequired + } else if lower.starts_with("timeout:") || lower.contains("timeout") { + RelayOutcomeKind::Timeout + } else if lower.starts_with("error:") { + RelayOutcomeKind::Error + } else { + RelayOutcomeKind::Unknown + }; + Self { + kind, + message: Some(message), + } + } + + pub(crate) fn timeout(message: impl Into<String>) -> Self { + Self { + kind: RelayOutcomeKind::Timeout, + message: Some(message.into()), + } + } + + pub(crate) fn connection_failed(message: impl Into<String>) -> Self { + Self { + kind: RelayOutcomeKind::ConnectionFailed, + message: Some(message.into()), + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct RelayPublishReceipt { + pub(crate) relay_url: String, + pub(crate) outcome: RelayOutcome, + pub(crate) attempted: bool, +} + +impl RelayPublishReceipt { + pub(crate) fn attempted(relay_url: impl Into<String>, outcome: RelayOutcome) -> Self { + Self { + relay_url: relay_url.into(), + outcome, + attempted: true, + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +pub(crate) struct RelayPublishError(pub(crate) String); + +pub(crate) trait RelayPublishAdapter: Send + Sync { + fn publish(&self, request: RelayPublishRequest) -> PublishFuture<'_>; +} + +#[derive(Clone)] +pub(crate) struct LiveRelayPublishAdapter { + client: DaemonNostrClient, +} + +impl LiveRelayPublishAdapter { + pub(crate) const fn new(client: DaemonNostrClient) -> Self { + Self { client } + } +} + +impl RelayPublishAdapter for LiveRelayPublishAdapter { + fn publish(&self, request: RelayPublishRequest) -> PublishFuture<'_> { + Box::pin(async move { + if request.now_ms < 0 { + return Err(RelayPublishError( + "publish timestamp must be nonnegative".to_owned(), + )); + } + let event = nostr::Event::from_json(request.signed_event.raw_json()) + .map_err(|error| RelayPublishError(error.to_string()))?; + let client = self.client.clone().into_inner(); + let mut receipts = Vec::with_capacity(request.targets.relays().len()); + for relay in request.targets.relays() { + let relay_url = relay.as_str().to_owned(); + let attempt = async { + client.add_relay(relay_url.as_str()).await?; + client + .try_connect_relay(relay_url.as_str(), Duration::from_secs(10)) + .await?; + client.send_event_to([relay_url.as_str()], &event).await + }; + let outcome = match attempt.await { + Ok(output) + if output.success.iter().any(|url| { + url.to_string().trim_end_matches('/') == relay_url.trim_end_matches('/') + }) => + { + RelayOutcome::accepted() + } + Ok(output) => output + .failed + .values() + .next() + .map(RelayOutcome::classify) + .unwrap_or_else(|| RelayOutcome::connection_failed("relay omitted result")), + Err(error) => RelayOutcome::connection_failed(error.to_string()), + }; + receipts.push(RelayPublishReceipt::attempted(relay_url, outcome)); + } + Ok(receipts) + }) + } +} + +#[cfg(test)] +#[derive(Clone, Default)] +pub(crate) struct MockRelayPublishAdapter { + outcomes: BTreeMap<String, RelayOutcome>, + captured_raw_events: Arc<Mutex<Vec<String>>>, +} + +#[cfg(test)] +impl MockRelayPublishAdapter { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn with_outcome( + mut self, + relay_url: impl Into<String>, + outcome: RelayOutcome, + ) -> Self { + self.outcomes.insert(relay_url.into(), outcome); + self + } + + pub(crate) fn captured_raw_events(&self) -> Vec<String> { + self.captured_raw_events + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } +} + +#[cfg(test)] +impl RelayPublishAdapter for MockRelayPublishAdapter { + fn publish(&self, request: RelayPublishRequest) -> PublishFuture<'_> { + Box::pin(async move { + self.captured_raw_events + .lock() + .map_err(|_| RelayPublishError("captured event lock poisoned".to_owned()))? + .push(request.signed_event.raw_json().to_owned()); + Ok(request + .targets + .relays() + .iter() + .map(|relay| { + RelayPublishReceipt::attempted( + relay.as_str(), + self.outcomes + .get(relay.as_str()) + .cloned() + .unwrap_or_else(RelayOutcome::accepted), + ) + }) + .collect()) + }) + } +} diff --git a/tests/source_boundary.rs b/tests/source_boundary.rs @@ -263,13 +263,12 @@ 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/transport_publish_protocol/src/lib.rs") + .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") .as_path(), ); for required in [ - "TransportPublishProtocolError::InvalidTransportKind { index }", - "RadrootsTransportKind::parse_canonical(self.transport_kind.as_str())", - "transport_kind_error(error, index)", + "\"local\" | \"nostr\" | \"reticulum\" => {}", + "_ => return Err(Error::InvalidTransportKind { index })", ] { assert!( protocol_source.contains(required), @@ -281,6 +280,7 @@ fn transport_publish_sources_reject_removed_execution_transport_targets() { for required in [ "publish_event_rejects_removed_execution_kind_before_recording_job", "publish_event_rejects_removed_execution_target_before_recording_job", + "!matches!(canonical.as_str(), \"local\" | \"nostr\" | \"reticulum\")", ] { assert!( daemon_source.contains(required), @@ -371,16 +371,17 @@ fn transport_publish_required_targets_stay_fingerprint_exact() { 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/transport_publish_protocol/src/lib.rs") + .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") .as_path(), ); for required in [ "validate_delivery_policy_for_resolution", - "delivery_policy\n .validate_target_membership(target_fingerprints.as_slice())", + "let target_fingerprints = resolution.target_fingerprints()?;", + ".any(|actual| actual.as_str() == required.as_str())", "fn required_outcomes_for_policy", "target_outcome_fingerprint(outcome, index)", - "fingerprint == required", + "fingerprint.as_str() == required.as_str()", "let required_outcomes = required_outcomes_for_policy(targets, outcomes);", "required_outcomes.len() == targets.len()", ] { @@ -392,13 +393,13 @@ fn transport_publish_required_targets_stay_fingerprint_exact() { let satisfaction_arm = source_window( daemon_source.as_str(), - "TransportPublishDeliveryPolicy::RequiredTargets { targets } => {", + "DeliveryPolicy::RequiredTargets { targets } => {", "fn delivery_status(", ); for required in [ "let nostr_required_targets = targets", "target\n .fingerprint()", - "filter(|fingerprint| fingerprint == required)", + "filter(|fingerprint| fingerprint.as_str() == required.as_str())", "RadrootsTransportSatisfactionPolicy::required_targets(", ] { assert!( @@ -418,8 +419,8 @@ fn transport_publish_required_targets_stay_fingerprint_exact() { } for required in [ - "pub fn validate_target_membership", - "TransportPublishProtocolError::RequiredTargetNotInTargetSet { index }", + "RequiredTargetNotInTargetSet", + "Matching fingerprints to native targets is intentionally deferred", "Self::RequiredTargets { targets } => targets.len()", ] { assert!( @@ -462,22 +463,22 @@ fn transport_publish_capabilities_expose_per_transport_readiness() { let protocol_source = read_source( Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../lib/crates/transport_publish_protocol/src/lib.rs") + .join("../lib/crates/protocol/src/radrootsd/transport_publish/v5.rs") .as_path(), ); for required in [ "pub transport: String,", "pub configured: bool,", - "pub implementation: TransportPublishImplementation,", + "pub implementation: Implementation,", "pub usable_for_delivery: bool,", - "pub capabilities: TransportPublishOperationCapabilities,", - "pub struct TransportPublishOperationCapabilities", + "pub capabilities: OperationCapabilities,", + "pub struct OperationCapabilities", "pub deliver: bool,", "pub fetch: bool,", "pub discovery: bool,", "pub gateway_forwarding: bool,", "pub receipt_observation: bool,", - "RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE", + "RETICULUM_UNAVAILABLE_MESSAGE", ] { assert!( protocol_source.contains(required), @@ -486,15 +487,12 @@ fn transport_publish_capabilities_expose_per_transport_readiness() { } let capability_source = source_window( protocol_source.as_str(), - "pub struct TransportPublishTransportCapability", - "#[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 TransportPublishDeliveryPolicyName", + "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: TransportPublishImplementationState," - ), + concat!("pub implementation", "_state: ImplementationState,"), ] { assert!( !capability_source.contains(forbidden), @@ -710,12 +708,15 @@ fn nip46_transport_uses_completed_events_and_exact_sign_event_binding() { "src/transport/jsonrpc/methods/nip46/connect.rs", "src/transport/jsonrpc/methods/nip46/session_authorize.rs", ] - .map(|path| read_source(manifest_dir.join(path).as_path())) + .map(|path| { + let source = read_source(manifest_dir.join(path).as_path()); + production_source(source.as_str()).to_owned() + }) .join("\n"); assert!(protocol.contains("EventBuilder::nostr_connect")); assert!(protocol.contains(".sign_with_keys(sender_keys)")); - assert!(!production_sources.contains("RadrootsNostrEventBuilder")); + assert!(!production_sources.contains("EventBuilder")); assert!(!production_sources.contains("radroots_nostr_build_event")); assert!(!production_sources.contains(".send_event_builder(")); for required in [