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:
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");