lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit b704b8030c8d6dbc82cc43bfdbfced327c0c6f09
parent c66740d5851ebff78dbe8d8d9e6fb5f03ee91d8e
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 11:49:45 +0000

sdk: isolate radrootsd execution adapter

- compile the adapter only behind the approved private radrootsd feature module
- expose inert explicit daemon configuration through the transport surface
- validate generation-five protocol requests responses and partial outcomes
- redact authentication RPC and malformed-response failures at the public edge

Diffstat:
Mcrates/sdk/Cargo.toml | 4++++
Mcrates/sdk/src/adapters/mod.rs | 9++-------
Mcrates/sdk/src/adapters/radrootsd.rs | 9++-------
Mcrates/sdk/src/lib.rs | 2++
Mcrates/sdk/src/transport.rs | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/package_boundary.rs | 14++++++++++++++
Mcrates/sdk/tests/unit/adapters_radrootsd_tests.rs | 11++++-------
7 files changed, 225 insertions(+), 21 deletions(-)

diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -37,6 +37,8 @@ local-signing = [ radrootsd = [ "sync", "dep:reqwest", + "dep:serde", + "dep:serde_json", ] geonames = ["dep:radroots_geonames"] knowledge = [ @@ -79,6 +81,8 @@ reqwest = { workspace = true, optional = true, default-features = false, feature "json", "rustls-tls", ] } +serde = { workspace = true, optional = true, features = ["derive"] } +serde_json = { workspace = true, optional = true, features = ["std"] } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/sdk/src/adapters/mod.rs b/crates/sdk/src/adapters/mod.rs @@ -1,7 +1,2 @@ -#[cfg(feature = "transport-nostr-client")] -pub mod nostr; -#[cfg(feature = "radrootsd-execution")] -pub mod radrootsd; -#[cfg(feature = "signer-adapters")] -#[doc(hidden)] -pub mod signer; +#[cfg(feature = "radrootsd")] +pub(crate) mod radrootsd; diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -35,7 +35,6 @@ pub struct RadrootsdPublishConfig { pub endpoint: String, pub auth: RadrootsdAuth, pub timeout: Duration, - pub request_timeout_ms: Option<u64>, } impl RadrootsdPublishConfig { @@ -44,7 +43,6 @@ impl RadrootsdPublishConfig { endpoint: endpoint.into(), auth: RadrootsdAuth::None, timeout: Duration::from_secs(10), - request_timeout_ms: None, } } @@ -57,11 +55,6 @@ impl RadrootsdPublishConfig { self.timeout = timeout; self } - - pub fn with_request_timeout_ms(mut self, timeout_ms: u64) -> Self { - self.request_timeout_ms = Some(timeout_ms); - self - } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -74,6 +67,7 @@ impl RadrootsdPublishAdapter { Self { config } } + #[cfg(test)] pub fn config(&self) -> &RadrootsdPublishConfig { &self.config } @@ -216,6 +210,7 @@ fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { } } +#[cfg(test)] pub fn publish_event_request_json( request: &TransportPublishEventRequest, ) -> Result<Value, RadrootsdError> { diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -6,6 +6,8 @@ #![forbid(unsafe_code)] +mod adapters; + pub mod capability; pub mod client; pub mod diagnostics; diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -132,6 +132,190 @@ impl Default for Profile { } } +/// Explicit daemon adapter authentication configuration. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum DaemonAuth { + /// Sends no authorization header. + None, + /// Sends the supplied bearer credential only when delivery is invoked. + BearerToken(String), +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Debug for DaemonAuth { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => formatter.write_str("None"), + Self::BearerToken(_) => formatter.write_str("BearerToken(<redacted>)"), + } + } +} + +/// Explicit daemon endpoint and request deadline configuration. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DaemonConfig { + endpoint: String, + auth: DaemonAuth, + timeout: core::time::Duration, +} + +#[cfg(feature = "radrootsd")] +impl DaemonConfig { + /// Creates inert configuration; no client is built and no request is sent. + #[must_use] + pub fn new(endpoint: impl Into<String>) -> Self { + Self { + endpoint: endpoint.into(), + auth: DaemonAuth::None, + timeout: core::time::Duration::from_secs(10), + } + } + + /// Selects explicit authentication for later invocation. + #[must_use] + pub fn with_auth(mut self, auth: DaemonAuth) -> Self { + self.auth = auth; + self + } + + /// Selects the complete HTTP/RPC request deadline. + #[must_use] + pub const fn with_timeout(mut self, timeout: core::time::Duration) -> Self { + self.timeout = timeout; + self + } +} + +/// Stable secret-safe daemon execution failure class. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum DaemonErrorKind { + /// The explicit authentication value cannot be represented safely. + Authentication, + /// The versioned protocol rejected the request. + InvalidRequest, + /// HTTP transport or timeout failed. + Transport, + /// The daemon returned a JSON-RPC error. + Rpc, + /// The response was malformed or did not match the request. + InvalidResponse, +} + +/// One redacted daemon failure retaining a private source chain. +#[cfg(feature = "radrootsd")] +pub struct DaemonError { + kind: DaemonErrorKind, + source: crate::adapters::radrootsd::RadrootsdError, +} + +#[cfg(feature = "radrootsd")] +impl DaemonError { + /// Returns the stable failure class. + #[must_use] + pub const fn kind(&self) -> DaemonErrorKind { + self.kind + } + + fn from_private(source: crate::adapters::radrootsd::RadrootsdError) -> Self { + use crate::adapters::radrootsd::RadrootsdError; + let kind = match &source { + RadrootsdError::InvalidAuthHeader(_) => DaemonErrorKind::Authentication, + RadrootsdError::InvalidRequest(_) => DaemonErrorKind::InvalidRequest, + RadrootsdError::Http(_) => DaemonErrorKind::Transport, + RadrootsdError::JsonRpc { .. } => DaemonErrorKind::Rpc, + RadrootsdError::MalformedResponse(_) => DaemonErrorKind::InvalidResponse, + }; + Self { kind, source } + } +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Display for DaemonError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self.kind { + DaemonErrorKind::Authentication => "daemon authentication configuration is invalid", + DaemonErrorKind::InvalidRequest => "daemon delivery request is invalid", + DaemonErrorKind::Transport => "daemon transport failed", + DaemonErrorKind::Rpc => "daemon RPC failed", + DaemonErrorKind::InvalidResponse => "daemon response is invalid", + }) + } +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Debug for DaemonError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("DaemonError") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +#[cfg(feature = "radrootsd")] +impl std::error::Error for DaemonError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +/// Explicitly configured daemon execution adapter. +/// +/// Construction is inert. Network contact occurs only in [`Self::deliver`]. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DaemonDelivery { + adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter, +} + +#[cfg(feature = "radrootsd")] +impl DaemonDelivery { + /// Creates an inert adapter from explicit host configuration. + #[must_use] + pub fn new(config: DaemonConfig) -> Self { + let auth = match config.auth { + DaemonAuth::None => crate::adapters::radrootsd::RadrootsdAuth::None, + DaemonAuth::BearerToken(token) => { + crate::adapters::radrootsd::RadrootsdAuth::BearerToken(token) + } + }; + Self { + adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter::new( + crate::adapters::radrootsd::RadrootsdPublishConfig::new(config.endpoint) + .with_auth(auth) + .with_timeout(config.timeout), + ), + } + } + + /// Invokes the generation-5 daemon transport-publish contract. + pub async fn deliver( + &self, + signed_event: radroots_event::SignedEvent, + target_policy: radroots_protocol::radrootsd::transport_publish::v5::TargetPolicy, + delivery_policy: radroots_protocol::radrootsd::transport_publish::v5::DeliveryPolicy, + idempotency_key: Option<String>, + timeout_ms: Option<u64>, + ) -> Result<radroots_protocol::radrootsd::transport_publish::v5::EventResponse, DaemonError> + { + self.adapter + .publish_signed_event(crate::adapters::radrootsd::RadrootsdPublishRequest { + signed_event, + target_policy, + delivery_policy, + idempotency_key, + timeout_ms, + }) + .await + .map_err(DaemonError::from_private) + } +} + #[cfg(test)] mod tests { use radroots_transport::{ @@ -234,4 +418,17 @@ mod tests { .all(|target| *target.kind() == TransportId::NOSTR) ); } + + #[cfg(feature = "radrootsd")] + #[test] + fn daemon_configuration_is_inert_explicit_and_redacted() { + let config = DaemonConfig::new("http://127.0.0.1:1/rpc") + .with_auth(DaemonAuth::BearerToken("secret-token".to_owned())) + .with_timeout(core::time::Duration::from_millis(5)); + let adapter = DaemonDelivery::new(config); + + let debug = format!("{adapter:?}"); + assert!(!debug.contains("secret-token")); + assert!(!debug.contains("reqwest")); + } } diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -8,6 +8,8 @@ const LISTING: &str = include_str!("../src/listing.rs"); const TRADE: &str = include_str!("../src/trade.rs"); const STORAGE: &str = include_str!("../src/storage.rs"); const DIAGNOSTICS: &str = include_str!("../src/diagnostics.rs"); +const ADAPTERS: &str = include_str!("../src/adapters/mod.rs"); +const RADROOTSD: &str = include_str!("../src/adapters/radrootsd.rs"); const SYNC: &str = include_str!("../src/sync.rs"); const TRANSPORT: &str = include_str!("../src/transport.rs"); @@ -345,6 +347,18 @@ fn reliability_and_diagnostics_return_canonical_storage_contracts() { } } +#[test] +fn radrootsd_adapter_is_private_explicit_versioned_and_redacted() { + assert!(ROOT.contains("mod adapters;")); + assert!(!ROOT.contains("pub mod adapters")); + assert!(ADAPTERS.contains("cfg(feature = \"radrootsd\")")); + assert!(ADAPTERS.contains("pub(crate) mod radrootsd")); + assert!(RADROOTSD.contains("transport_publish::v5")); + assert!(RADROOTSD.contains("reqwest::Client::builder")); + assert!(RADROOTSD.contains("BearerToken(<redacted>)")); + assert!(!RADROOTSD.contains("tokio::spawn")); +} + fn dependency_names(manifest: &str) -> BTreeSet<&str> { let dependencies = manifest .split_once("[dependencies]") diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs @@ -10,7 +10,7 @@ use radroots_protocol::radrootsd::transport_publish::v5::{ TargetOutcome as TransportPublishTargetOutcome, TargetPolicy as TransportPublishTargetPolicy, TargetSource as TransportPublishTargetSource, }; -use radroots_transport_reticulum::RADROOTS_RETICULUM_ENDPOINT_URI; +const RADROOTS_RETICULUM_ENDPOINT_URI: &str = "reticulum:local"; use std::io::{Read, Write}; use std::net::TcpListener; use std::thread::JoinHandle; @@ -348,8 +348,7 @@ fn auth_headers_omit_or_redact_bearer_authorization() { fn radrootsd_publish_config_builders_preserve_typed_runtime_options() { let config = RadrootsdPublishConfig::new("http://127.0.0.1:8080/rpc") .with_auth(RadrootsdAuth::BearerToken("sdk-token".to_owned())) - .with_timeout(Duration::from_millis(250)) - .with_request_timeout_ms(1_500); + .with_timeout(Duration::from_millis(250)); let adapter = RadrootsdPublishAdapter::new(config.clone()); assert_eq!(adapter.config(), &config); @@ -359,7 +358,6 @@ fn radrootsd_publish_config_builders_preserve_typed_runtime_options() { RadrootsdAuth::BearerToken("sdk-token".to_owned()) ); assert_eq!(adapter.config().timeout, Duration::from_millis(250)); - assert_eq!(adapter.config().request_timeout_ms, Some(1_500)); } #[test] @@ -501,8 +499,7 @@ async fn publish_signed_event_posts_typed_radrootsd_request() { let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str()); let adapter = RadrootsdPublishAdapter::new( RadrootsdPublishConfig::new(endpoint) - .with_auth(RadrootsdAuth::BearerToken("sdk-token".into())) - .with_request_timeout_ms(7_000), + .with_auth(RadrootsdAuth::BearerToken("sdk-token".into())), ); let receipt = adapter @@ -515,7 +512,7 @@ async fn publish_signed_event_posts_typed_radrootsd_request() { ]), delivery_policy: TransportPublishDeliveryPolicy::All, idempotency_key: Some("idem-typed".to_owned()), - timeout_ms: adapter.config().request_timeout_ms, + timeout_ms: Some(7_000), }) .await .expect("typed publish");