lib

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

commit 4a5e94fd492c0d1b870cf45d262f0d340625ea13
parent 70cfc205200e56f45dbfa76b983b50b7db13bbb2
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 10:12:14 +0000

sdk: align package manifest and module root

- declare the final required and optional lower package graph
- replace legacy feature aliases with the governed vocabulary
- establish the eleven-module SDK ownership skeleton
- verify default and all-feature package boundaries

Diffstat:
Mcrates/sdk/Cargo.toml | 242++++++++++++++-----------------------------------------------------------------
Acrates/sdk/src/capability.rs | 1+
Acrates/sdk/src/client.rs | 1+
Acrates/sdk/src/diagnostics.rs | 1+
Mcrates/sdk/src/error.rs | 1350+------------------------------------------------------------------------------
Mcrates/sdk/src/farm.rs | 10+---------
Mcrates/sdk/src/lib.rs | 213++++++-------------------------------------------------------------------------
Acrates/sdk/src/listing.rs | 1+
Acrates/sdk/src/signing.rs | 1+
Acrates/sdk/src/storage.rs | 1+
Acrates/sdk/src/sync.rs | 1+
Acrates/sdk/src/trade.rs | 1+
Mcrates/sdk/src/transport.rs | 774+------------------------------------------------------------------------------
Acrates/sdk/tests/package_boundary.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 141 insertions(+), 2528 deletions(-)

diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -6,236 +6,80 @@ edition.workspace = true authors.workspace = true rust-version.workspace = true license.workspace = true -description = "Public Rust SDK for Radroots" +description = "Host-neutral asynchronous client engine for Radroots" repository.workspace = true homepage.workspace = true readme = "README.md" +autotests = false +autoexamples = false [lib] name = "radroots_sdk" [features] -default = ["std", "serde", "serde_json", "identity-models"] -std = [ - "radroots_event/std", - "radroots_event_codec/std", - "radroots_trade/std", -] -serde = [ - "dep:serde", - "radroots_event/serde", - "radroots_trade/serde", -] -serde_json = [ - "dep:serde_json", - "serde", - "nostr", - "radroots_event_codec/json", - "radroots_trade/serde_json", -] -nostr = [] -knowledge = [ - "serde", - "serde_json", - "radroots_event/knowledge", - "radroots_event_codec/manifests", -] -identity-models = [ - "radroots_identity/std", -] -signing = [ - "dep:nostr", +default = ["memory"] +memory = ["radroots_storage/memory"] +sqlite = ["dep:radroots_storage_sqlite"] +sync = ["dep:radroots_sync"] +nostr = [ + "sync", "dep:radroots_nostr", - "dep:radroots_protocol", - "nostr", - "radroots_nostr/std", - "radroots_protocol/std", - "radroots_signing/std", -] -transport-nostr-client = [ - "signing", - "std", - "serde_json", - "dep:radroots_transport_nostr", - "radroots_transport_nostr/client", -] -radrootsd-execution = [ - "std", - "serde_json", - "dep:futures", - "dep:radroots_protocol", - "dep:radroots_transport", "dep:radroots_transport_nostr", - "dep:reqwest", - "radroots_protocol/serde", - "radroots_protocol/std", - "radroots_transport/serde", - "radroots_transport_nostr/std", ] -signer-adapters = [ - "identity-models", - "local-signer", - "signing", - "std", +nip46 = [ + "nostr", "dep:radroots_nostr_connect", - "dep:radroots_nostr_signer", - "dep:tokio", - "radroots_nostr/events", ] -runtime = [ - "std", - "serde_json", - "dep:tokio", - "dep:base64", - "dep:hex", - "dep:radroots_event_store", - "dep:radroots_geocoder", - "dep:radroots_outbox", - "dep:radroots_protected_store", - "dep:radroots_protocol", - "dep:radroots_transport", - "dep:radroots_transport_nostr", - "dep:radroots_transport_reticulum", - "dep:radroots_runtime_paths", - "dep:radroots_secret_vault", - "dep:sha2", - "dep:sqlx", - "dep:uuid", - "radroots_signing/std", - "radroots_event_store/sqlite", - "radroots_event_store/runtime-tokio", - "radroots_outbox/sqlite", - "radroots_outbox/runtime-tokio", - "radroots_protected_store/std", - "radroots_protocol/std", - "radroots_secret_vault/std", - "radroots_transport/serde", - "radroots_transport_nostr/std", - "radroots_transport_nostr/storage", - "radroots_transport_nostr/runtime-tokio", -] -local-signer = [ - "runtime", - "signing", +local-signing = [ "dep:radroots_secrets", "radroots_nostr/signing", ] -transport-nostr-runtime = [ - "runtime", - "dep:radroots_nostr", - "radroots_transport_nostr/client", -] -local-runtime = [ - "std", - "serde", - "serde_json", - "runtime", - "signer-adapters", - "transport-nostr-runtime", - "transport-nostr-client", +radrootsd = [ + "sync", + "dep:reqwest", ] -local-runtime-radrootsd-execution = [ - "std", - "serde", - "serde_json", - "runtime", - "signer-adapters", - "radrootsd-execution", +geonames = ["dep:radroots_geonames"] +knowledge = [ + "radroots_event/knowledge", + "radroots_event_codec/knowledge", ] -test-fixture-geonames-asset = [ - "runtime", - "radroots_geocoder/test-fixture-geonames-asset", +native = ["sqlite", "sync", "local-signing"] +full = [ + "native", + "nostr", + "nip46", + "radrootsd", + "geonames", + "knowledge", ] [dependencies] -base64 = { workspace = true, optional = true } -futures = { workspace = true, optional = true } -radroots_event_store = { workspace = true, optional = true, default-features = false } +radroots_core = { workspace = true, default-features = false } radroots_event = { workspace = true, default-features = false } radroots_event_codec = { workspace = true, default-features = false } -radroots_geocoder = { workspace = true, optional = true } -radroots_outbox = { workspace = true, optional = true, default-features = false } -radroots_protocol = { workspace = true, optional = true, default-features = false } -radroots_protected_store = { workspace = true, optional = true, default-features = false } -radroots_transport = { workspace = true, optional = true, default-features = false } -radroots_transport_nostr = { workspace = true, optional = true, default-features = false } -radroots_transport_reticulum = { workspace = true, optional = true, default-features = false } -radroots_runtime_paths = { workspace = true, optional = true, default-features = false } -radroots_secret_vault = { workspace = true, optional = true, default-features = false } -radroots_secrets = { workspace = true, optional = true, default-features = false } +radroots_identity = { workspace = true, default-features = false } +radroots_protocol = { workspace = true, default-features = false } radroots_signing = { workspace = true, default-features = false } +radroots_storage = { workspace = true, default-features = false } radroots_trade = { workspace = true, default-features = false } -radroots_identity = { workspace = true, default-features = false } +radroots_transport = { workspace = true, default-features = false } + +radroots_geonames = { workspace = true, optional = true, default-features = false } radroots_nostr = { workspace = true, optional = true, default-features = false } -radroots_nostr_connect = { workspace = true, optional = true } -radroots_nostr_signer = { workspace = true, optional = true, default-features = false } +radroots_nostr_connect = { workspace = true, optional = true, default-features = false } +radroots_secrets = { workspace = true, optional = true, default-features = false } +radroots_storage_sqlite = { workspace = true, optional = true, default-features = false } +radroots_sync = { workspace = true, optional = true, default-features = false } +radroots_transport_nostr = { workspace = true, optional = true, default-features = false } + reqwest = { workspace = true, optional = true, default-features = false, features = [ "json", "rustls-tls", ] } -hex = { workspace = true, optional = true } -nostr = { workspace = true, optional = true } -serde = { workspace = true, optional = true, default-features = false, features = [ - "derive", - "alloc", -] } -serde_json = { workspace = true, optional = true, default-features = false, features = [ - "alloc", -] } -sha2 = { workspace = true, optional = true } -sqlx = { version = "0.9.0", optional = true, default-features = false, features = [ - "runtime-tokio", - "sqlite-bundled", -] } -tokio = { workspace = true, optional = true, features = ["macros", "rt", "sync", "time"] } -uuid = { workspace = true, optional = true } - -[[example]] -name = "sdk_v1_listing_prepare" -path = "examples/sdk_v1_listing_prepare.rs" -required-features = ["runtime"] - -[[example]] -name = "sdk_v1_knowledge_prepare" -path = "examples/sdk_v1_knowledge_prepare.rs" -required-features = ["knowledge"] - -[[example]] -name = "sdk_v1_local_enqueue_and_mock_sync" -path = "examples/sdk_v1_local_enqueue_and_mock_sync.rs" -required-features = ["runtime", "signer-adapters"] - -[[example]] -name = "sdk_v1_myc_nip46_signer_setup" -path = "examples/sdk_v1_myc_nip46_signer_setup.rs" -required-features = ["runtime", "signer-adapters"] -[[example]] -name = "runtime_local" -path = "examples/runtime_local.rs" -required-features = ["local-runtime"] - -[dev-dependencies] -futures = { workspace = true } -nostr = { workspace = true } -radroots_core = { workspace = true, default-features = false, features = [ - "std", -] } -radroots_signing = { workspace = true, default-features = false, features = ["std"] } -radroots_replica_store = { workspace = true, default-features = false, features = [ - "native", -] } -radroots_replica_schema = { workspace = true } -radroots_replica_sync = { workspace = true, features = ["std", "legacy-ingest"] } -radroots_sql_core = { workspace = true, features = ["native"] } -radroots_nostr = { workspace = true, default-features = false, features = [ - "std", - "events", - "signing", -] } -tempfile = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -tokio-tungstenite = "0.26.2" +[[test]] +name = "package_boundary" +path = "tests/package_boundary.rs" [lints] workspace = true diff --git a/crates/sdk/src/capability.rs b/crates/sdk/src/capability.rs @@ -0,0 +1 @@ +//! Client capability reporting. diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -0,0 +1 @@ +//! Client construction and lifecycle. diff --git a/crates/sdk/src/diagnostics.rs b/crates/sdk/src/diagnostics.rs @@ -0,0 +1 @@ +//! Redacted client diagnostics. diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -1,1349 +1 @@ -#[cfg(feature = "runtime")] -use std::{fmt, path::PathBuf}; - -#[cfg(feature = "runtime")] -use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField}; -#[cfg(feature = "runtime")] -use crate::transport::ReticulumBehavior; -#[cfg(feature = "runtime")] -use radroots_event::trade::validation::OperationalListingValidationError; -#[cfg(feature = "runtime")] -use serde_json::{Value, json}; - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkErrorClass { - Authorization, - Clock, - Configuration, - Request, - Storage, - Transport, - Unsupported, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkRecoveryAction { - InspectLocalStores, - InspectGeoNamesAsset, - RetryOperationWithSameIdempotencyKey, - ConfigureTransportTargets, - ConfigureGeoNamesCache, - ConfigureSigner, - FixRequest, - SelectAuthorizedActor, - CompleteSignerAuthentication, - RetryAfterTransportFailure, - RetryGeoNamesDownload, - EnableRequiredFeature, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkGeoNamesErrorKind { - Configuration, - Download, - Cache, - Integrity, - Schema, - Lookup, -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkListingValidationErrorKind { - InvalidKind, - InvalidProfile, - MissingListingId, - ListingEventNotFound, - ListingEventFetchFailed, - ParseError, - InvalidSeller, - MissingFarmProfile, - MissingFarmRecord, - MissingTitle, - MissingDescription, - MissingProductType, - MissingBins, - MissingPrimaryBin, - InvalidBin, - MissingPrice, - InvalidPrice, - MissingInventory, - InvalidInventory, - MissingAvailability, - MissingLocation, - MissingLocationLocality, - MissingLocationGeohash, - InvalidLocationGeohash, - MissingDeliveryMethod, -} - -#[cfg(feature = "runtime")] -impl From<&OperationalListingValidationError> for RadrootsSdkListingValidationErrorKind { - fn from(error: &OperationalListingValidationError) -> Self { - match error { - OperationalListingValidationError::InvalidKind { .. } => Self::InvalidKind, - OperationalListingValidationError::InvalidProfile => Self::InvalidProfile, - OperationalListingValidationError::MissingListingId => Self::MissingListingId, - OperationalListingValidationError::ListingEventNotFound { .. } => { - Self::ListingEventNotFound - } - OperationalListingValidationError::ListingEventFetchFailed { .. } => { - Self::ListingEventFetchFailed - } - OperationalListingValidationError::ParseError { .. } => Self::ParseError, - OperationalListingValidationError::InvalidSeller => Self::InvalidSeller, - OperationalListingValidationError::MissingFarmProfile => Self::MissingFarmProfile, - OperationalListingValidationError::MissingFarmRecord => Self::MissingFarmRecord, - OperationalListingValidationError::MissingTitle => Self::MissingTitle, - OperationalListingValidationError::MissingDescription => Self::MissingDescription, - OperationalListingValidationError::MissingProductType => Self::MissingProductType, - OperationalListingValidationError::MissingBins => Self::MissingBins, - OperationalListingValidationError::MissingPrimaryBin => Self::MissingPrimaryBin, - OperationalListingValidationError::InvalidBin => Self::InvalidBin, - OperationalListingValidationError::MissingPrice => Self::MissingPrice, - OperationalListingValidationError::InvalidPrice => Self::InvalidPrice, - OperationalListingValidationError::MissingInventory => Self::MissingInventory, - OperationalListingValidationError::InvalidInventory => Self::InvalidInventory, - OperationalListingValidationError::MissingAvailability => Self::MissingAvailability, - OperationalListingValidationError::MissingLocation => Self::MissingLocation, - OperationalListingValidationError::MissingLocationLocality => { - Self::MissingLocationLocality - } - OperationalListingValidationError::MissingLocationGeohash => { - Self::MissingLocationGeohash - } - OperationalListingValidationError::InvalidLocationGeohash => { - Self::InvalidLocationGeohash - } - OperationalListingValidationError::MissingDeliveryMethod => Self::MissingDeliveryMethod, - } - } -} - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkTradeErrorKind { - InvalidEnvelope, - InvalidCommandBody, - PrivateArtifactMissing, - PrivateArtifactCommitmentMismatch, - PrivateArtifactAcknowledgementMissing, - TradeNotFound, - QueryLimitInvalid, - CursorInvalid, -} - -#[cfg(feature = "runtime")] -impl RadrootsSdkTradeErrorKind { - pub fn code(self) -> &'static str { - match self { - Self::InvalidEnvelope => "trade_invalid_envelope", - Self::InvalidCommandBody => "trade_invalid_command_body", - Self::PrivateArtifactMissing => "trade_private_artifact_missing", - Self::PrivateArtifactCommitmentMismatch => "trade_private_artifact_commitment_mismatch", - Self::PrivateArtifactAcknowledgementMissing => { - "trade_private_artifact_acknowledgement_missing" - } - Self::TradeNotFound => "trade_not_found", - Self::QueryLimitInvalid => "trade_query_limit_invalid", - Self::CursorInvalid => "trade_cursor_invalid", - } - } -} - -#[cfg(feature = "runtime")] -const RECOVERY_INSPECT_LOCAL_STORES: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::InspectLocalStores]; -#[cfg(feature = "runtime")] -const RECOVERY_INSPECT_GEONAMES_ASSET: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::InspectGeoNamesAsset]; -#[cfg(feature = "runtime")] -const RECOVERY_RETRY_OPERATION_WITH_SAME_IDEMPOTENCY_KEY: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey]; -#[cfg(feature = "runtime")] -const RECOVERY_CONFIGURE_TRANSPORT_TARGETS: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::ConfigureTransportTargets]; -#[cfg(feature = "runtime")] -const RECOVERY_CONFIGURE_GEONAMES_CACHE: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::ConfigureGeoNamesCache]; -#[cfg(feature = "runtime")] -const RECOVERY_CONFIGURE_SIGNER: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::ConfigureSigner]; -#[cfg(feature = "runtime")] -const RECOVERY_FIX_REQUEST: &[RadrootsSdkRecoveryAction] = &[RadrootsSdkRecoveryAction::FixRequest]; -#[cfg(feature = "runtime")] -const RECOVERY_SELECT_AUTHORIZED_ACTOR: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::SelectAuthorizedActor]; -#[cfg(feature = "runtime")] -const RECOVERY_COMPLETE_SIGNER_AUTHENTICATION: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::CompleteSignerAuthentication]; -#[cfg(feature = "runtime")] -const RECOVERY_RETRY_AFTER_TRANSPORT_FAILURE: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::RetryAfterTransportFailure]; -#[cfg(feature = "runtime")] -const RECOVERY_RETRY_GEONAMES_DOWNLOAD: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::RetryGeoNamesDownload]; -#[cfg(feature = "runtime")] -const RECOVERY_ENABLE_REQUIRED_FEATURE: &[RadrootsSdkRecoveryAction] = - &[RadrootsSdkRecoveryAction::EnableRequiredFeature]; - -#[cfg(feature = "runtime")] -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -pub struct RadrootsSdkErrorCatalogEntry { - pub code: &'static str, - pub class: RadrootsSdkErrorClass, - pub retryable: bool, - pub recovery_actions: &'static [RadrootsSdkRecoveryAction], -} - -#[cfg(feature = "runtime")] -impl RadrootsSdkErrorCatalogEntry { - const fn new( - code: &'static str, - class: RadrootsSdkErrorClass, - retryable: bool, - recovery_actions: &'static [RadrootsSdkRecoveryAction], - ) -> Self { - Self { - code, - class, - retryable, - recovery_actions, - } - } -} - -#[cfg(feature = "runtime")] -pub const RADROOTS_SDK_ERROR_CATALOG: &[RadrootsSdkErrorCatalogEntry] = &[ - RadrootsSdkErrorCatalogEntry::new( - "io", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "clock_before_unix_epoch", - RadrootsSdkErrorClass::Clock, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "timestamp_out_of_range", - RadrootsSdkErrorClass::Clock, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "unauthorized_actor", - RadrootsSdkErrorClass::Authorization, - false, - RECOVERY_SELECT_AUTHORIZED_ACTOR, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_pubkey_mismatch", - RadrootsSdkErrorClass::Authorization, - false, - RECOVERY_SELECT_AUTHORIZED_ACTOR, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_unavailable", - RadrootsSdkErrorClass::Configuration, - false, - RECOVERY_CONFIGURE_SIGNER, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_request_rejected", - RadrootsSdkErrorClass::Authorization, - false, - RECOVERY_SELECT_AUTHORIZED_ACTOR, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_request_timed_out", - RadrootsSdkErrorClass::Transport, - true, - RECOVERY_RETRY_AFTER_TRANSPORT_FAILURE, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_auth_challenge_pending", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_COMPLETE_SIGNER_AUTHENTICATION, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_transport", - RadrootsSdkErrorClass::Transport, - true, - RECOVERY_RETRY_AFTER_TRANSPORT_FAILURE, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_protocol", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "signer_returned_event_drift", - RadrootsSdkErrorClass::Authorization, - false, - RECOVERY_CONFIGURE_SIGNER, - ), - RadrootsSdkErrorCatalogEntry::new( - "empty_transport_targets", - RadrootsSdkErrorClass::Configuration, - false, - RECOVERY_CONFIGURE_TRANSPORT_TARGETS, - ), - RadrootsSdkErrorCatalogEntry::new( - "transport_target_limit_exceeded", - RadrootsSdkErrorClass::Configuration, - false, - RECOVERY_CONFIGURE_TRANSPORT_TARGETS, - ), - RadrootsSdkErrorCatalogEntry::new( - "invalid_relay_url", - RadrootsSdkErrorClass::Configuration, - false, - RECOVERY_CONFIGURE_TRANSPORT_TARGETS, - ), - RadrootsSdkErrorCatalogEntry::new( - "idempotency_conflict", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_RETRY_OPERATION_WITH_SAME_IDEMPOTENCY_KEY, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_invalid_envelope", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_invalid_command_body", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_private_artifact_missing", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_private_artifact_commitment_mismatch", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_private_artifact_acknowledgement_missing", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_not_found", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_query_limit_invalid", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "trade_cursor_invalid", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "privacy_preflight", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "product_sync_unsupported", - RadrootsSdkErrorClass::Unsupported, - false, - RECOVERY_ENABLE_REQUIRED_FEATURE, - ), - RadrootsSdkErrorCatalogEntry::new( - "reticulum_transport_unavailable", - RadrootsSdkErrorClass::Unsupported, - false, - RECOVERY_CONFIGURE_TRANSPORT_TARGETS, - ), - RadrootsSdkErrorCatalogEntry::new( - "reticulum_transport_deferred", - RadrootsSdkErrorClass::Unsupported, - false, - RECOVERY_CONFIGURE_TRANSPORT_TARGETS, - ), - RadrootsSdkErrorCatalogEntry::new( - "product_sync_transport_setup_failure", - RadrootsSdkErrorClass::Transport, - true, - RECOVERY_RETRY_AFTER_TRANSPORT_FAILURE, - ), - RadrootsSdkErrorCatalogEntry::new( - "authority", - RadrootsSdkErrorClass::Authorization, - false, - RECOVERY_SELECT_AUTHORIZED_ACTOR, - ), - RadrootsSdkErrorCatalogEntry::new( - "event_store", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "invalid_request", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "unsupported_profile_schema", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "listing_edit", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "listing_validation", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "listing_mutation", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "outbox", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "private_store", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "studio_store", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_configuration", - RadrootsSdkErrorClass::Configuration, - false, - RECOVERY_CONFIGURE_GEONAMES_CACHE, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_download", - RadrootsSdkErrorClass::Transport, - true, - RECOVERY_RETRY_GEONAMES_DOWNLOAD, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_cache", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_GEONAMES_ASSET, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_integrity", - RadrootsSdkErrorClass::Storage, - false, - RECOVERY_INSPECT_GEONAMES_ASSET, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_schema", - RadrootsSdkErrorClass::Storage, - false, - RECOVERY_INSPECT_GEONAMES_ASSET, - ), - RadrootsSdkErrorCatalogEntry::new( - "geonames_lookup", - RadrootsSdkErrorClass::Request, - false, - RECOVERY_FIX_REQUEST, - ), - RadrootsSdkErrorCatalogEntry::new( - "transport", - RadrootsSdkErrorClass::Transport, - true, - RECOVERY_RETRY_AFTER_TRANSPORT_FAILURE, - ), - RadrootsSdkErrorCatalogEntry::new( - "projection", - RadrootsSdkErrorClass::Storage, - true, - RECOVERY_INSPECT_LOCAL_STORES, - ), -]; - -#[cfg(feature = "runtime")] -pub fn radroots_sdk_error_catalog() -> &'static [RadrootsSdkErrorCatalogEntry] { - RADROOTS_SDK_ERROR_CATALOG -} - -#[cfg(feature = "runtime")] -#[derive(Debug)] -#[non_exhaustive] -pub enum RadrootsSdkError { - Io { - path: PathBuf, - message: String, - }, - ClockBeforeUnixEpoch, - TimestampOutOfRange { - value: u64, - }, - UnauthorizedActor { - operation: String, - reason: String, - }, - SignerPubkeyMismatch { - operation: String, - expected_pubkey_prefix: String, - signer_pubkey_prefix: String, - }, - SignerUnavailable { - mode: String, - reason: String, - }, - SignerRequestRejected { - mode: String, - reason: String, - }, - SignerRequestTimedOut { - mode: String, - }, - SignerAuthChallengePending { - mode: String, - auth_url: Option<String>, - }, - SignerTransport { - mode: String, - reason: String, - }, - SignerProtocol { - mode: String, - reason: String, - }, - SignerReturnedEventDrift { - operation: String, - reason: String, - }, - EmptyTransportTargets { - operation: String, - }, - TransportTargetLimitExceeded { - max: usize, - actual: usize, - }, - InvalidRelayUrl { - url: String, - reason: String, - }, - IdempotencyConflict { - operation_kind: String, - expected_pubkey_prefix: String, - existing_digest_prefix: String, - new_digest_prefix: String, - }, - Trade { - kind: RadrootsSdkTradeErrorKind, - operation: String, - message: String, - }, - PrivacyPreflight { - operation: String, - status: PrivacyPreflightStatus, - fields: Vec<ProductSensitivityField>, - }, - ProductSyncUnsupported { - operation: &'static str, - required_feature: &'static str, - }, - ReticulumTransportUnavailable { - operation: String, - endpoint_uri: String, - behavior: ReticulumBehavior, - }, - ProductSyncTransportSetupFailure { - message: String, - }, - Authority { - message: String, - }, - EventStore { - message: String, - }, - InvalidRequest { - message: String, - }, - UnsupportedProfileSchema { - path: PathBuf, - message: String, - }, - ListingEdit { - message: String, - }, - ListingValidation { - kind: RadrootsSdkListingValidationErrorKind, - message: String, - }, - ListingMutation { - message: String, - }, - Outbox { - message: String, - }, - PrivateStore { - message: String, - }, - StudioStore { - message: String, - }, - GeoNames { - kind: RadrootsSdkGeoNamesErrorKind, - message: String, - }, - Transport { - message: String, - }, - Projection { - message: String, - }, -} - -#[cfg(feature = "runtime")] -impl RadrootsSdkError { - pub fn code(&self) -> &'static str { - match self { - Self::Io { .. } => "io", - Self::ClockBeforeUnixEpoch => "clock_before_unix_epoch", - Self::TimestampOutOfRange { .. } => "timestamp_out_of_range", - Self::UnauthorizedActor { .. } => "unauthorized_actor", - Self::SignerPubkeyMismatch { .. } => "signer_pubkey_mismatch", - Self::SignerUnavailable { .. } => "signer_unavailable", - Self::SignerRequestRejected { .. } => "signer_request_rejected", - Self::SignerRequestTimedOut { .. } => "signer_request_timed_out", - Self::SignerAuthChallengePending { .. } => "signer_auth_challenge_pending", - Self::SignerTransport { .. } => "signer_transport", - Self::SignerProtocol { .. } => "signer_protocol", - Self::SignerReturnedEventDrift { .. } => "signer_returned_event_drift", - Self::EmptyTransportTargets { .. } => "empty_transport_targets", - Self::TransportTargetLimitExceeded { .. } => "transport_target_limit_exceeded", - Self::InvalidRelayUrl { .. } => "invalid_relay_url", - Self::IdempotencyConflict { .. } => "idempotency_conflict", - Self::Trade { kind, .. } => kind.code(), - Self::PrivacyPreflight { .. } => "privacy_preflight", - Self::ProductSyncUnsupported { .. } => "product_sync_unsupported", - Self::ReticulumTransportUnavailable { behavior, .. } => match behavior { - ReticulumBehavior::RejectDeliveryAttempts => "reticulum_transport_unavailable", - ReticulumBehavior::DeferDeliveryPlans => "reticulum_transport_deferred", - }, - Self::ProductSyncTransportSetupFailure { .. } => "product_sync_transport_setup_failure", - Self::Authority { .. } => "authority", - Self::EventStore { .. } => "event_store", - Self::InvalidRequest { .. } => "invalid_request", - Self::UnsupportedProfileSchema { .. } => "unsupported_profile_schema", - Self::ListingEdit { .. } => "listing_edit", - Self::ListingValidation { .. } => "listing_validation", - Self::ListingMutation { .. } => "listing_mutation", - Self::Outbox { .. } => "outbox", - Self::PrivateStore { .. } => "private_store", - Self::StudioStore { .. } => "studio_store", - Self::GeoNames { kind, .. } => match kind { - RadrootsSdkGeoNamesErrorKind::Configuration => "geonames_configuration", - RadrootsSdkGeoNamesErrorKind::Download => "geonames_download", - RadrootsSdkGeoNamesErrorKind::Cache => "geonames_cache", - RadrootsSdkGeoNamesErrorKind::Integrity => "geonames_integrity", - RadrootsSdkGeoNamesErrorKind::Schema => "geonames_schema", - RadrootsSdkGeoNamesErrorKind::Lookup => "geonames_lookup", - }, - Self::Transport { .. } => "transport", - Self::Projection { .. } => "projection", - } - } - - pub fn class(&self) -> RadrootsSdkErrorClass { - match self { - Self::Io { .. } - | Self::EventStore { .. } - | Self::Outbox { .. } - | Self::PrivateStore { .. } - | Self::StudioStore { .. } - | Self::Projection { .. } => RadrootsSdkErrorClass::Storage, - Self::GeoNames { kind, .. } => match kind { - RadrootsSdkGeoNamesErrorKind::Configuration => RadrootsSdkErrorClass::Configuration, - RadrootsSdkGeoNamesErrorKind::Download => RadrootsSdkErrorClass::Transport, - RadrootsSdkGeoNamesErrorKind::Cache - | RadrootsSdkGeoNamesErrorKind::Integrity - | RadrootsSdkGeoNamesErrorKind::Schema => RadrootsSdkErrorClass::Storage, - RadrootsSdkGeoNamesErrorKind::Lookup => RadrootsSdkErrorClass::Request, - }, - Self::ClockBeforeUnixEpoch | Self::TimestampOutOfRange { .. } => { - RadrootsSdkErrorClass::Clock - } - Self::UnauthorizedActor { .. } - | Self::SignerPubkeyMismatch { .. } - | Self::SignerRequestRejected { .. } - | Self::SignerReturnedEventDrift { .. } - | Self::Authority { .. } => RadrootsSdkErrorClass::Authorization, - Self::SignerUnavailable { .. } => RadrootsSdkErrorClass::Configuration, - Self::EmptyTransportTargets { .. } - | Self::TransportTargetLimitExceeded { .. } - | Self::InvalidRelayUrl { .. } => RadrootsSdkErrorClass::Configuration, - Self::IdempotencyConflict { .. } - | Self::Trade { .. } - | Self::PrivacyPreflight { .. } - | Self::SignerProtocol { .. } - | Self::SignerAuthChallengePending { .. } - | Self::InvalidRequest { .. } - | Self::UnsupportedProfileSchema { .. } - | Self::ListingEdit { .. } - | Self::ListingValidation { .. } - | Self::ListingMutation { .. } => RadrootsSdkErrorClass::Request, - Self::ProductSyncUnsupported { .. } | Self::ReticulumTransportUnavailable { .. } => { - RadrootsSdkErrorClass::Unsupported - } - Self::ProductSyncTransportSetupFailure { .. } - | Self::Transport { .. } - | Self::SignerRequestTimedOut { .. } - | Self::SignerTransport { .. } => RadrootsSdkErrorClass::Transport, - } - } - - pub fn retryable(&self) -> bool { - matches!( - self, - Self::Io { .. } - | Self::ProductSyncTransportSetupFailure { .. } - | Self::EventStore { .. } - | Self::Outbox { .. } - | Self::PrivateStore { .. } - | Self::StudioStore { .. } - | Self::GeoNames { - kind: RadrootsSdkGeoNamesErrorKind::Cache - | RadrootsSdkGeoNamesErrorKind::Download, - .. - } - | Self::Transport { .. } - | Self::SignerRequestTimedOut { .. } - | Self::SignerTransport { .. } - | Self::Projection { .. } - ) - } - - pub fn recovery_actions(&self) -> Vec<RadrootsSdkRecoveryAction> { - match self { - Self::Io { .. } - | Self::EventStore { .. } - | Self::Outbox { .. } - | Self::PrivateStore { .. } - | Self::StudioStore { .. } - | Self::Projection { .. } => vec![RadrootsSdkRecoveryAction::InspectLocalStores], - Self::GeoNames { kind, .. } => match kind { - RadrootsSdkGeoNamesErrorKind::Configuration => { - vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache] - } - RadrootsSdkGeoNamesErrorKind::Download => { - vec![RadrootsSdkRecoveryAction::RetryGeoNamesDownload] - } - RadrootsSdkGeoNamesErrorKind::Cache - | RadrootsSdkGeoNamesErrorKind::Integrity - | RadrootsSdkGeoNamesErrorKind::Schema => { - vec![RadrootsSdkRecoveryAction::InspectGeoNamesAsset] - } - RadrootsSdkGeoNamesErrorKind::Lookup => vec![RadrootsSdkRecoveryAction::FixRequest], - }, - Self::UnauthorizedActor { .. } - | Self::SignerPubkeyMismatch { .. } - | Self::SignerRequestRejected { .. } - | Self::Authority { .. } => vec![RadrootsSdkRecoveryAction::SelectAuthorizedActor], - Self::SignerUnavailable { .. } | Self::SignerReturnedEventDrift { .. } => { - vec![RadrootsSdkRecoveryAction::ConfigureSigner] - } - Self::EmptyTransportTargets { .. } - | Self::TransportTargetLimitExceeded { .. } - | Self::InvalidRelayUrl { .. } => { - vec![RadrootsSdkRecoveryAction::ConfigureTransportTargets] - } - Self::IdempotencyConflict { .. } => { - vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey] - } - Self::Trade { .. } => vec![RadrootsSdkRecoveryAction::FixRequest], - Self::PrivacyPreflight { .. } => vec![RadrootsSdkRecoveryAction::FixRequest], - Self::UnsupportedProfileSchema { .. } => { - vec![RadrootsSdkRecoveryAction::InspectLocalStores] - } - Self::ProductSyncUnsupported { .. } => { - vec![RadrootsSdkRecoveryAction::EnableRequiredFeature] - } - Self::ReticulumTransportUnavailable { .. } => { - vec![RadrootsSdkRecoveryAction::ConfigureTransportTargets] - } - Self::ProductSyncTransportSetupFailure { .. } | Self::Transport { .. } => { - vec![RadrootsSdkRecoveryAction::RetryAfterTransportFailure] - } - Self::SignerRequestTimedOut { .. } | Self::SignerTransport { .. } => { - vec![RadrootsSdkRecoveryAction::RetryAfterTransportFailure] - } - Self::SignerAuthChallengePending { .. } => { - vec![RadrootsSdkRecoveryAction::CompleteSignerAuthentication] - } - Self::ClockBeforeUnixEpoch - | Self::TimestampOutOfRange { .. } - | Self::SignerProtocol { .. } - | Self::InvalidRequest { .. } - | Self::ListingEdit { .. } - | Self::ListingValidation { .. } - | Self::ListingMutation { .. } => vec![RadrootsSdkRecoveryAction::FixRequest], - } - } - - pub fn detail_json(&self) -> Value { - let detail = match self { - Self::Io { path, message } => { - json!({ "path": path.display().to_string(), "message": message }) - } - Self::ClockBeforeUnixEpoch => json!({}), - Self::TimestampOutOfRange { value } => json!({ "value": value }), - Self::UnauthorizedActor { operation, reason } => { - json!({ "operation": operation, "reason": reason }) - } - Self::SignerPubkeyMismatch { - operation, - expected_pubkey_prefix, - signer_pubkey_prefix, - } => json!({ - "operation": operation, - "expected_pubkey_prefix": expected_pubkey_prefix, - "signer_pubkey_prefix": signer_pubkey_prefix - }), - Self::SignerUnavailable { mode, reason } - | Self::SignerRequestRejected { mode, reason } - | Self::SignerTransport { mode, reason } - | Self::SignerProtocol { mode, reason } => { - json!({ "mode": mode, "reason": reason }) - } - Self::SignerRequestTimedOut { mode } => json!({ "mode": mode }), - Self::SignerAuthChallengePending { mode, auth_url } => { - json!({ "mode": mode, "auth_url": auth_url }) - } - Self::SignerReturnedEventDrift { operation, reason } => { - json!({ "operation": operation, "reason": reason }) - } - Self::EmptyTransportTargets { operation } => json!({ "operation": operation }), - Self::TransportTargetLimitExceeded { max, actual } => { - json!({ "max": max, "actual": actual }) - } - Self::InvalidRelayUrl { url, reason } => json!({ "url": url, "reason": reason }), - Self::IdempotencyConflict { - operation_kind, - expected_pubkey_prefix, - existing_digest_prefix, - new_digest_prefix, - } => json!({ - "operation_kind": operation_kind, - "expected_pubkey_prefix": expected_pubkey_prefix, - "existing_digest_prefix": existing_digest_prefix, - "new_digest_prefix": new_digest_prefix - }), - Self::Trade { - kind, - operation, - message, - } => json!({ - "kind": kind, - "operation": operation, - "message": message - }), - Self::PrivacyPreflight { - operation, - status, - fields, - } => json!({ - "operation": operation, - "status": status, - "fields": fields - }), - Self::ProductSyncUnsupported { - operation, - required_feature, - } => json!({ "operation": operation, "required_feature": required_feature }), - Self::ReticulumTransportUnavailable { - operation, - endpoint_uri, - behavior, - } => json!({ - "operation": operation, - "endpoint_uri": endpoint_uri, - "behavior": behavior.as_str() - }), - Self::ListingValidation { kind, message } => { - json!({ "kind": kind, "message": message }) - } - Self::ProductSyncTransportSetupFailure { message } - | Self::Authority { message } - | Self::EventStore { message } - | Self::InvalidRequest { message } - | Self::ListingEdit { message } - | Self::ListingMutation { message } - | Self::Outbox { message } - | Self::PrivateStore { message } - | Self::StudioStore { message } - | Self::Transport { message } - | Self::Projection { message } => json!({ "message": message }), - Self::UnsupportedProfileSchema { path, message } => { - json!({ "path": path.display().to_string(), "message": message }) - } - Self::GeoNames { kind, message } => json!({ "kind": kind, "message": message }), - }; - json!({ - "code": self.code(), - "class": self.class(), - "retryable": self.retryable(), - "message": self.to_string(), - "recovery_actions": self.recovery_actions(), - "detail": detail - }) - } - - pub(crate) fn empty_transport_targets(operation: impl Into<String>) -> Self { - Self::EmptyTransportTargets { - operation: operation.into(), - } - } - - pub(crate) fn transport_target_limit_exceeded(max: usize, actual: usize) -> Self { - Self::TransportTargetLimitExceeded { max, actual } - } - - pub(crate) fn invalid_relay_url(url: impl Into<String>, reason: impl Into<String>) -> Self { - Self::InvalidRelayUrl { - url: redacted_relay_url(url.into()), - reason: reason.into(), - } - } - - pub(crate) fn missing_geonames_config() -> Self { - Self::GeoNames { - kind: RadrootsSdkGeoNamesErrorKind::Configuration, - message: "GeoNames cache root is not configured".to_owned(), - } - } -} - -#[cfg(feature = "runtime")] -impl fmt::Display for RadrootsSdkError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Io { path, message } => { - write!(f, "sdk storage path `{}` failed: {message}", path.display()) - } - Self::ClockBeforeUnixEpoch => f.write_str("sdk clock is before the Unix epoch"), - Self::TimestampOutOfRange { value } => { - write!( - f, - "sdk timestamp {value} exceeds Nostr u32 created_at range" - ) - } - Self::UnauthorizedActor { operation, reason } => { - write!(f, "sdk unauthorized actor for {operation}: {reason}") - } - Self::SignerPubkeyMismatch { - operation, - expected_pubkey_prefix, - signer_pubkey_prefix, - } => write!( - f, - "sdk signer pubkey mismatch for {operation}: expected_pubkey_prefix={expected_pubkey_prefix}, signer_pubkey_prefix={signer_pubkey_prefix}" - ), - Self::SignerUnavailable { mode, reason } => { - write!(f, "sdk {mode} signer unavailable: {reason}") - } - Self::SignerRequestRejected { mode, reason } => { - write!(f, "sdk {mode} signer rejected request: {reason}") - } - Self::SignerRequestTimedOut { mode } => { - write!(f, "sdk {mode} signer request timed out") - } - Self::SignerAuthChallengePending { mode, auth_url } => match auth_url { - Some(auth_url) => { - write!(f, "sdk {mode} signer requires authentication at {auth_url}") - } - None => write!(f, "sdk {mode} signer requires authentication"), - }, - Self::SignerTransport { mode, reason } => { - write!(f, "sdk {mode} signer transport error: {reason}") - } - Self::SignerProtocol { mode, reason } => { - write!(f, "sdk {mode} signer protocol error: {reason}") - } - Self::SignerReturnedEventDrift { operation, reason } => { - write!( - f, - "sdk signer returned event drift for {operation}: {reason}" - ) - } - Self::EmptyTransportTargets { operation } => { - write!(f, "sdk empty transport targets for {operation}") - } - Self::TransportTargetLimitExceeded { max, actual } => { - write!( - f, - "sdk transport target limit exceeded: max={max}, actual={actual}" - ) - } - Self::InvalidRelayUrl { url, reason } => { - write!(f, "sdk invalid relay URL `{url}`: {reason}") - } - Self::IdempotencyConflict { - operation_kind, - expected_pubkey_prefix, - existing_digest_prefix, - new_digest_prefix, - } => write!( - f, - "sdk idempotency conflict for {operation_kind}: expected_pubkey_prefix={expected_pubkey_prefix}, existing_digest_prefix={existing_digest_prefix}, new_digest_prefix={new_digest_prefix}" - ), - Self::Trade { - kind, - operation, - message, - } => write!(f, "sdk trade error for {operation} ({kind:?}): {message}"), - Self::PrivacyPreflight { - operation, - status, - fields, - } => write!( - f, - "sdk privacy preflight failed for {operation}: status={status:?}, field_count={}", - fields.len() - ), - Self::ProductSyncUnsupported { - operation, - required_feature, - } => write!( - f, - "sdk product sync operation {operation} requires feature `{required_feature}`" - ), - Self::ReticulumTransportUnavailable { - operation, - endpoint_uri, - behavior, - } => write!( - f, - "sdk product sync operation {operation} cannot deliver through Reticulum endpoint `{endpoint_uri}` with behavior `{}`", - behavior.as_str() - ), - Self::ProductSyncTransportSetupFailure { message } => { - write!(f, "sdk product sync transport setup failed: {message}") - } - Self::Authority { message } => write!(f, "sdk authority error: {message}"), - Self::EventStore { message } => write!(f, "sdk event store error: {message}"), - Self::InvalidRequest { message } => write!(f, "sdk invalid request: {message}"), - Self::UnsupportedProfileSchema { path, message } => write!( - f, - "sdk unsupported profile schema at `{}`: {message}", - path.display() - ), - Self::ListingEdit { message } => write!(f, "sdk listing edit error: {message}"), - Self::ListingValidation { kind, message } => { - write!(f, "sdk listing validation error ({kind:?}): {message}") - } - Self::ListingMutation { message } => { - write!(f, "sdk listing mutation error: {message}") - } - Self::Outbox { message } => write!(f, "sdk outbox error: {message}"), - Self::PrivateStore { message } => write!(f, "sdk private store error: {message}"), - Self::StudioStore { message } => write!(f, "sdk studio store error: {message}"), - Self::GeoNames { kind, message } => { - write!(f, "sdk GeoNames {kind:?} error: {message}") - } - Self::Transport { message } => { - write!(f, "sdk transport error: {message}") - } - Self::Projection { message } => write!(f, "sdk projection error: {message}"), - } - } -} - -#[cfg(feature = "runtime")] -impl std::error::Error for RadrootsSdkError {} - -#[cfg(feature = "runtime")] -impl From<radroots_signing::Error> for RadrootsSdkError { - fn from(error: radroots_signing::Error) -> Self { - use radroots_signing::error::Kind; - - match error.kind() { - Kind::AuthorizationDenied => Self::UnauthorizedActor { - operation: "event signing".to_owned(), - reason: "actor or signer is not authorized for the frozen draft".to_owned(), - }, - Kind::SignerCapabilityMissing => Self::SignerUnavailable { - mode: "configured".to_owned(), - reason: error.to_string(), - }, - Kind::SignerUnavailable => Self::SignerUnavailable { - mode: "configured".to_owned(), - reason: error.to_string(), - }, - Kind::SignerRejected => Self::SignerRequestRejected { - mode: "configured".to_owned(), - reason: error.to_string(), - }, - Kind::SignerTimeout | Kind::DeadlineExceeded => Self::SignerRequestTimedOut { - mode: "configured".to_owned(), - }, - Kind::SignerCancelled => Self::SignerRequestRejected { - mode: "configured".to_owned(), - reason: error.to_string(), - }, - Kind::SignerOutputInvalid => Self::SignerReturnedEventDrift { - operation: "event signing".to_owned(), - reason: error.to_string(), - }, - Kind::InvalidArgument | Kind::InternalError => Self::Authority { - message: error.to_string(), - }, - _ => Self::Authority { - message: error.to_string(), - }, - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_event_store::RadrootsEventStoreError> for RadrootsSdkError { - fn from(error: radroots_event_store::RadrootsEventStoreError) -> Self { - Self::EventStore { - message: error.to_string(), - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_geocoder::GeocoderError> for RadrootsSdkError { - fn from(error: radroots_geocoder::GeocoderError) -> Self { - let kind = match &error { - radroots_geocoder::GeocoderError::InvalidAssetUrl { .. } - | radroots_geocoder::GeocoderError::InvalidAssetHost { .. } => { - RadrootsSdkGeoNamesErrorKind::Configuration - } - radroots_geocoder::GeocoderError::AssetDownload { .. } => { - RadrootsSdkGeoNamesErrorKind::Download - } - radroots_geocoder::GeocoderError::Io(_) - | radroots_geocoder::GeocoderError::Sqlite(_) - | radroots_geocoder::GeocoderError::SqliteConnectionLockUnavailable - | radroots_geocoder::GeocoderError::AssetLockUnavailable { .. } => { - RadrootsSdkGeoNamesErrorKind::Cache - } - radroots_geocoder::GeocoderError::InvalidAssetSchema { .. } => { - RadrootsSdkGeoNamesErrorKind::Schema - } - radroots_geocoder::GeocoderError::InvalidAssetLength { .. } - | radroots_geocoder::GeocoderError::InvalidAssetSha256 { .. } - | radroots_geocoder::GeocoderError::InvalidAssetSqlite { .. } - | radroots_geocoder::GeocoderError::InvalidAssetIntegrity { .. } => { - RadrootsSdkGeoNamesErrorKind::Integrity - } - radroots_geocoder::GeocoderError::CountryCenterNotFound { .. } => { - RadrootsSdkGeoNamesErrorKind::Lookup - } - }; - Self::GeoNames { - kind, - message: error.to_string(), - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_trade::operational_listing::RadrootsOperationalListingEditError> - for RadrootsSdkError -{ - fn from( - error: radroots_trade::operational_listing::RadrootsOperationalListingEditError, - ) -> Self { - match error { - radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidModel(error) => Self::ListingValidation { - kind: RadrootsSdkListingValidationErrorKind::from(&error), - message: error.to_string(), - }, - error => Self::ListingEdit { - message: error.to_string(), - }, - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_trade::operational_listing::RadrootsOperationalListingMutationError> - for RadrootsSdkError -{ - fn from( - error: radroots_trade::operational_listing::RadrootsOperationalListingMutationError, - ) -> Self { - Self::ListingMutation { - message: error.to_string(), - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError { - fn from(error: radroots_outbox::RadrootsOutboxError) -> Self { - match error { - radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets => { - Self::empty_transport_targets("outbox enqueue") - } - radroots_outbox::RadrootsOutboxError::IdempotencyConflict { - operation_kind, - expected_pubkey, - existing_digest, - new_digest, - .. - } => Self::IdempotencyConflict { - operation_kind, - expected_pubkey_prefix: redacted_prefix(expected_pubkey.as_str()), - existing_digest_prefix: redacted_prefix(existing_digest.as_str()), - new_digest_prefix: redacted_prefix(new_digest.as_str()), - }, - error => Self::Outbox { - message: error.to_string(), - }, - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_transport::RadrootsTransportError> for RadrootsSdkError { - fn from(error: radroots_transport::RadrootsTransportError) -> Self { - match error { - radroots_transport::RadrootsTransportError::EmptyTargetSet => { - Self::empty_transport_targets("transport target set") - } - error => Self::Transport { - message: error.to_string(), - }, - } - } -} - -#[cfg(feature = "runtime")] -impl From<radroots_transport_nostr::RadrootsRelayTransportError> for RadrootsSdkError { - fn from(error: radroots_transport_nostr::RadrootsRelayTransportError) -> Self { - match error { - radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlParse { - url, - reason, - } => Self::invalid_relay_url(url, reason), - radroots_transport_nostr::RadrootsRelayTransportError::WsRequiresLocalhostPolicy { - url, - } => Self::invalid_relay_url(url, "ws relay URL requires localhost policy"), - radroots_transport_nostr::RadrootsRelayTransportError::UnsupportedRelayScheme { - url, - scheme, - } => Self::invalid_relay_url(url, format!("unsupported scheme `{scheme}`")), - radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlUserinfo { url } => { - Self::invalid_relay_url(url, "relay URL must not include userinfo") - } - radroots_transport_nostr::RadrootsRelayTransportError::EmptyRelayHost { url } => { - Self::invalid_relay_url(url, "relay URL must include a host") - } - radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlQueryOrFragment { - url, - } => Self::invalid_relay_url(url, "relay URL must not include query or fragment"), - radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlForbiddenDestination { - url, - reason, - } => Self::invalid_relay_url(url, reason), - radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { - url, - address, - reason, - } => Self::invalid_relay_url( - url, - format!("relay URL resolved to forbidden address `{address}`: {reason}"), - ), - radroots_transport_nostr::RadrootsRelayTransportError::EmptyTargetSet => { - Self::empty_transport_targets("nostr relay publish") - } - #[cfg(feature = "runtime")] - radroots_transport_nostr::RadrootsRelayTransportError::Outbox(error) => error.into(), - error => Self::Transport { - message: error.to_string(), - }, - } - } -} - -#[cfg(feature = "runtime")] -fn redacted_prefix(value: &str) -> String { - value.chars().take(12).collect() -} - -#[cfg(feature = "runtime")] -fn redacted_relay_url(value: String) -> String { - let redacted = redact_query_or_fragment(value.as_str()); - let Some((scheme, rest)) = redacted.split_once("://") else { - return redacted; - }; - let authority = rest.split('/').next().unwrap_or(rest); - let Some((_, after_userinfo)) = authority.rsplit_once('@') else { - return redacted; - }; - let path = rest.strip_prefix(authority).unwrap_or_default(); - format!("{scheme}://<redacted>@{after_userinfo}{path}") -} - -#[cfg(feature = "runtime")] -fn redact_query_or_fragment(value: &str) -> String { - let Some((index, marker)) = value.char_indices().find_map(|(index, character)| { - matches!(character, '?' | '#').then_some((index, character)) - }) else { - return value.to_owned(); - }; - format!("{}{}<redacted>", &value[..index], marker) -} - -#[cfg(test)] -#[path = "../tests/unit/error_tests.rs"] -mod tests; +//! SDK-owned error boundary. diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs @@ -1,9 +1 @@ -pub use radroots_event::farm::*; - -pub use radroots_event_codec::encode::EventEncodeError; - -use radroots_event::wire::Nip01EventWireParts; - -pub fn build_draft(farm: &Farm) -> Result<Nip01EventWireParts, EventEncodeError> { - radroots_event_codec::encode::farm::to_wire_parts(farm) -} +//! Farm product operations. diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -1,201 +1,18 @@ -#![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unsafe_code)] +//! Host-neutral asynchronous client engine for Radroots. +//! +//! This checkpoint establishes the final package and module ownership +//! boundary. Client behavior is introduced through the ordered SDK refactor. -#[cfg(not(feature = "std"))] -extern crate alloc; +#![forbid(unsafe_code)] -#[cfg(feature = "runtime")] -mod actor_json; -#[cfg(any( - feature = "radrootsd-execution", - feature = "transport-nostr-client", - feature = "signer-adapters" -))] -pub mod adapters; -#[cfg(feature = "runtime")] -mod error; -#[cfg(feature = "runtime")] -mod farm; -#[cfg(feature = "runtime")] -mod farms_runtime; -#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))] -#[path = "../tests/support/fixture_signer.rs"] -pub(crate) mod fixture_signer; -#[cfg(feature = "runtime")] -mod geonames; -#[cfg(feature = "runtime")] -mod idempotency; -#[cfg(feature = "identity-models")] -pub mod identity; -#[cfg(feature = "knowledge")] -pub mod knowledge; -#[cfg(feature = "runtime")] -mod listings_runtime; -#[cfg(feature = "runtime")] -mod privacy; -#[cfg(feature = "runtime")] -mod private_store; -#[cfg(feature = "runtime")] -mod product_clients; -#[cfg(feature = "runtime")] -mod runtime; -#[cfg(all(test, feature = "runtime"))] -#[path = "../tests/support/serializer_failure.rs"] -pub(crate) mod serializer_failure; -#[cfg(all(feature = "runtime", feature = "signer-adapters"))] -mod signer_provider; -#[cfg(feature = "runtime")] -mod studio_store; -#[cfg(feature = "runtime")] -mod sync_runtime; -#[cfg(feature = "runtime")] -mod trade_runtime; -#[cfg(feature = "runtime")] +pub mod capability; +pub mod client; +pub mod diagnostics; +pub mod error; +pub mod farm; +pub mod listing; +pub mod signing; +pub mod storage; +pub mod sync; +pub mod trade; pub mod transport; -#[cfg(feature = "runtime")] -mod workflow_runtime; - -#[cfg(feature = "runtime")] -pub use crate::error::{ - RADROOTS_SDK_ERROR_CATALOG, RadrootsSdkError, RadrootsSdkErrorCatalogEntry, - RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, RadrootsSdkListingValidationErrorKind, - RadrootsSdkRecoveryAction, RadrootsSdkTradeErrorKind, radroots_sdk_error_catalog, -}; -#[cfg(feature = "runtime")] -pub use crate::farms_runtime::{ - FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmEnqueueReceipt, - FarmPreparePublishRequest, FarmPrivateLocationClearReceipt, FarmPrivateLocationClearRequest, - FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate, FarmPrivateLocationLookupReceipt, - FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest, FarmPrivateLocationSetResult, - FarmPrivateLocationUpsertRequest, FarmPublishPlan, SdkExactLocation, SdkPublicLocality, -}; -#[cfg(feature = "runtime")] -pub use crate::geonames::{ - GEONAMES_1_0_ASSET, GEONAMES_ASSET_BYTE_SIZE, GEONAMES_ASSET_FILE_NAME, GEONAMES_ASSET_HOST, - GEONAMES_ASSET_SHA256, GEONAMES_ASSET_URL, GEONAMES_ASSET_VERSION, GeoNamesAssetFetcher, - GeoNamesAssetSpec, GeoNamesAssetState, GeoNamesAssetStatus, GeoNamesBlockingHttpFetcher, - Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderLocalityCandidate, - GeocoderLocalityInput, GeocoderLocalityLookup, GeocoderLocalityQuery, GeocoderPoint, - GeocoderReverseOptions, GeocoderReverseResult, GeocoderStructuredLocalityQuery, - RadrootsGeoNamesConfig, -}; -#[cfg(feature = "runtime")] -pub use crate::idempotency::{ - SDK_IDEMPOTENCY_KEY_MAX_LEN, SdkIdempotencyKey, SdkTradeIdempotencyRecord, -}; -#[cfg(feature = "knowledge")] -pub use crate::knowledge::{ - AddressableRef, DraftError, EventDraft, EventEnvelope, EventRef, KIND_FILE_METADATA, - KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION, - KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, KIND_WIKI_MERGE_REQUEST, - KIND_WIKI_REDIRECT, KNOWLEDGE_CLAIM_CONTRACT_ID, KNOWLEDGE_FIELD_REPORT_CONTRACT_ID, - KNOWLEDGE_RELATION_CONTRACT_ID, KNOWLEDGE_REVIEW_CONTRACT_ID, KNOWLEDGE_SOURCE_CONTRACT_ID, - KnowledgeChangeProposal, KnowledgeCitationSpan, KnowledgeClaim, KnowledgeCodec, - KnowledgeDraftBuilder, KnowledgeEventBuilder, KnowledgeFieldContext, KnowledgeFieldReport, - KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation, - KnowledgeObservationValue, KnowledgeRelation, KnowledgeReview, KnowledgeReviewScope, - KnowledgeReviewScore, KnowledgeReviewTarget, KnowledgeSource, Nip01EventWireParts, - RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA, - RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, - RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, - RADROOTS_KNOWLEDGE_RELATION_SCHEMA, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, - RADROOTS_KNOWLEDGE_SCHEMA_VERSION, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, - RADROOTS_WIKI_D_TAG_MAX_LEN, RadrootsContractValidatedEvent, RadrootsDecodeError, - RadrootsDecodedEvent, RadrootsEncodeError, RadrootsIdVerifiedEvent, - RadrootsKnowledgeBuilderError, RadrootsKnowledgeClaimBuilder, - RadrootsKnowledgeContractManifest, RadrootsKnowledgeContractManifestEntry, - RadrootsKnowledgeFieldReportBuilder, RadrootsKnowledgeManifestCodecSupport, - RadrootsKnowledgeManifestDiscriminator, RadrootsKnowledgeManifestTagContract, - RadrootsKnowledgeRelationBuilder, RadrootsKnowledgeReviewBuilder, - RadrootsKnowledgeSourceBuilder, RadrootsNip01VerificationError, RadrootsSdkKnowledgeError, - RadrootsSignatureVerifiedEvent, RadrootsWikiArticleBuilder, RadrootsWikiMergeRequestBuilder, - RadrootsWikiRedirectBuilder, RightsAssertion, WIKI_ARTICLE_CONTRACT_ID, - WIKI_MERGE_REQUEST_CONTRACT_ID, WIKI_REDIRECT_CONTRACT_ID, WikiArticle, WikiArticleVersionRef, - WikiDTagError, WikiMergeRequest, WikiRedirect, build_knowledge_claim_event, - build_knowledge_field_report_event, build_knowledge_relation_event, - build_knowledge_review_event, build_knowledge_source_event, build_wiki_article_event, - build_wiki_merge_request_event, build_wiki_redirect_event, contract_manifest, - contract_manifest_json, contract_manifest_sha256, normalize_wiki_d_tag, - prepare_knowledge_claim_draft, prepare_knowledge_field_report_draft, - prepare_knowledge_relation_draft, prepare_knowledge_review_draft, - prepare_knowledge_source_draft, prepare_wiki_article_draft, prepare_wiki_merge_request_draft, - prepare_wiki_redirect_draft, validate_wiki_d_tag, verify_and_decode_radroots_event, -}; -#[cfg(feature = "runtime")] -pub use crate::listings_runtime::{ - LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, ListingEnqueueReceipt, - ListingPreparePublishRequest, ListingPublishPlan, SdkMutationState, -}; -#[cfg(feature = "runtime")] -pub use crate::privacy::{ - PrivacyPreflightConfirmation, PrivacyPreflightReceipt, PrivacyPreflightStatus, - ProductSensitivityField, -}; -#[cfg(feature = "runtime")] -pub use crate::product_clients::{ - FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient, TradesClient, -}; -#[cfg(feature = "runtime")] -pub use crate::runtime::{ - BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, RadrootsClient, - RadrootsClientBuilder, RadrootsSdkClock, RadrootsSdkStorageConfig, RadrootsSdkStoragePaths, - RadrootsSdkTimestamp, RestoreArchive, RestoreReceipt, RestoreRequest, SdkBackupKeyReference, - SdkBackupManifest, SdkBackupManifestKind, SdkBackupMemberHash, SdkBackupMemberHashes, - SdkBackupRecoveryManifest, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus, - SdkOutboxStorageStatus, SdkPrivateStoreStorageStatus, SdkRestoreFinalization, SdkRestoreState, - SdkSqliteStoreStatus, SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, - SdkStudioStoreStorageStatus, StorageCheckpointReceipt, StorageCheckpointRequest, - StorageStatusReceipt, StorageStatusRequest, -}; -#[cfg(all(feature = "runtime", feature = "signer-adapters"))] -#[doc(hidden)] -pub use crate::signer_provider::{ - RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS, - RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS, RadrootsSdkLocalKeySigner, - RadrootsSdkLocalSignerCapability, RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, - RadrootsSdkNip46ClientKey, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, - RadrootsSdkSignReceipt, RadrootsSdkSignRequest, RadrootsSdkSignerCapability, - RadrootsSdkSignerMode, RadrootsSdkSignerProgress, RadrootsSdkSignerProgressSink, - RadrootsSdkSignerProvider, RadrootsSdkSignerState, RadrootsSdkSignerStatus, - radroots_sdk_myc_nip46_product_permission_strings, radroots_sdk_myc_nip46_product_permissions, -}; -#[cfg(feature = "runtime")] -pub use crate::sync_runtime::{ - PUSH_OUTBOX_DEFAULT_CLAIM_TTL_MS, PUSH_OUTBOX_DEFAULT_LIMIT, - PUSH_OUTBOX_DEFAULT_NEXT_ATTEMPT_DELAY_MS, PUSH_OUTBOX_MAX_LIMIT, PushOutboxEventReceipt, - PushOutboxEventState, PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, - PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, ReticulumTryNowRequest, - SYNC_PROJECTION_REFRESH_DEFAULT_LIMIT, SYNC_PROJECTION_REFRESH_MAX_LIMIT, SdkRelayAuthPolicy, - SyncEventStoreStatus, SyncOutboxStatus, SyncProjectionRefreshReceipt, - SyncProjectionRefreshRequest, SyncStatusReceipt, SyncStatusRequest, SyncStatusSource, - SyncTransportOperationCapabilitiesSummary, SyncTransportProfileSummary, - SyncTransportStatusSummary, SyncTransportTargetSummary, -}; -#[cfg(feature = "runtime")] -pub use crate::trade_runtime::{ - CancelTradeRequest, DecideCandidateRequest, EvidenceRefreshReceipt, EvidenceView, - GetTradeRequest, InspectEvidenceRequest, ListTradesRequest, Page, ProposeRevisionRequest, - RefreshTradeEvidenceRequest, ResumeOperationRequest, SubmitProposalRequest, - TRADE_CANCEL_OPERATION_KIND, TRADE_DECIDE_CANDIDATE_OPERATION_KIND, - TRADE_PROPOSE_REVISION_OPERATION_KIND, TRADE_QUERY_DEFAULT_LIMIT, TRADE_QUERY_MAX_LIMIT, - TRADE_RESUME_OPERATION_KIND, TRADE_RUNTIME_CAPABILITY_API_VERSION, - TRADE_RUNTIME_PRIVATE_STORAGE_PROFILE_ID, TRADE_RUNTIME_PROTOCOL_PROFILE_ID, - TRADE_RUNTIME_STORAGE_PROFILE_ID, TRADE_RUNTIME_WIRE_PROFILE_ID, - TRADE_SUBMIT_PROPOSAL_OPERATION_KIND, TradeCommandLifecycleState, TradeCommandReceipt, - TradeCommandService, TradeCoreMvpCapabilityReport, TradeListAnyOf, TradeListFilter, - TradeListSort, TradeOptionalIntegrationCapabilityReport, TradePrivateArtifactDeleteReceipt, - TradePrivateArtifactDeleteRequest, TradePrivateArtifactKind, TradePrivateArtifactOpenReceipt, - TradePrivateArtifactOpenRequest, TradePrivateArtifactSealReceipt, - TradePrivateArtifactSealRequest, TradePrivateTermsAvailabilityView, - TradeProtocolCapabilityReport, TradeQueryService, TradeRuntimeCapabilityReport, - TradeStatusView, TradeStorageCapabilityReport, TradeSummaryView, -}; -#[cfg(feature = "runtime")] -pub use crate::transport::{ - MeshScopeId, MultiTargetProfile, NostrProfile, NostrRelayUrlPolicy, PublishMode, - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportOutcome, RadrootsTransportSatisfactionClass, RadrootsTransportTargetReceipt, - RadrootsdExecutionAuth, RadrootsdExecutionProfile, ReticulumAgentEndpoint, ReticulumBehavior, - ReticulumProfile, SDK_TRANSPORT_TARGET_MAX_COUNT, SatisfactionPolicy, TargetPolicy, TargetSet, - TransportId, TransportProfile, TransportReceipt, -}; diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs @@ -0,0 +1 @@ +//! Listing product operations. diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs @@ -0,0 +1 @@ +//! Signing capability composition. diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs @@ -0,0 +1 @@ +//! Storage capability composition. diff --git a/crates/sdk/src/sync.rs b/crates/sdk/src/sync.rs @@ -0,0 +1 @@ +//! Synchronization capability composition. diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -0,0 +1 @@ +//! Trade commands and queries. diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -1,773 +1 @@ -use crate::RadrootsSdkError; -use radroots_transport::target::{TargetFingerprint, TargetScope, TargetSet as TransportTargetSet}; -use radroots_transport::{ - RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, - RadrootsTransportImplementationState, RadrootsTransportSatisfactionPolicy, - RadrootsTransportStatus, Target, -}; -use radroots_transport_nostr::{RadrootsRelayUrlPolicy, RelayUrl}; -use radroots_transport_reticulum::{ - RADROOTS_RETICULUM_ENDPOINT_URI, RADROOTS_RETICULUM_SCOPE_ID, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, -}; -use serde::ser::{SerializeStruct, Serializer}; -use std::collections::BTreeSet; - -pub use radroots_transport::{ - RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryTargetStatus, - RadrootsTransportOutcome, RadrootsTransportSatisfactionClass, RadrootsTransportTargetReceipt, - TransportId, -}; - -pub const SDK_TRANSPORT_TARGET_MAX_COUNT: usize = 20; -pub const RETICULUM_AGENT_ENDPOINT_PREFIX: &str = "reticulum-agent:"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum PublishMode { - DryRun, - EnqueueOnly, - EnqueueAndPublish, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum SatisfactionPolicy { - NoWait, - AnyAccepted, - AllAccepted, - QuorumAccepted { - threshold: u16, - }, - AnyDelivered, - AllDelivered, - QuorumDelivered { - threshold: u16, - }, - RequiredAcceptedTargets { - target_fingerprints: Vec<TargetFingerprint>, - }, - RequiredDeliveredTargets { - target_fingerprints: Vec<TargetFingerprint>, - }, -} - -impl SatisfactionPolicy { - pub fn quorum_accepted(threshold: u16) -> Result<Self, RadrootsSdkError> { - validate_satisfaction_threshold(threshold)?; - Ok(Self::QuorumAccepted { threshold }) - } - - pub fn quorum_delivered(threshold: u16) -> Result<Self, RadrootsSdkError> { - validate_satisfaction_threshold(threshold)?; - Ok(Self::QuorumDelivered { threshold }) - } - - pub fn required_accepted_targets<I, S>(targets: I) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self::RequiredAcceptedTargets { - target_fingerprints: required_target_fingerprints(targets)?, - }) - } - - pub fn required_delivered_targets<I, S>(targets: I) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self::RequiredDeliveredTargets { - target_fingerprints: required_target_fingerprints(targets)?, - }) - } - - pub fn is_no_wait(&self) -> bool { - matches!(self, Self::NoWait) - } - - pub(crate) fn transport_satisfaction_policy( - &self, - ) -> Result<RadrootsTransportSatisfactionPolicy, RadrootsSdkError> { - Ok(match self { - Self::NoWait => RadrootsTransportSatisfactionPolicy::no_wait(), - Self::AnyAccepted => RadrootsTransportSatisfactionPolicy::any_accepted(), - Self::AllAccepted => RadrootsTransportSatisfactionPolicy::all_accepted(), - Self::QuorumAccepted { threshold } => { - RadrootsTransportSatisfactionPolicy::quorum_accepted(*threshold) - } - Self::AnyDelivered => RadrootsTransportSatisfactionPolicy::any_delivered(), - Self::AllDelivered => RadrootsTransportSatisfactionPolicy::all_delivered(), - Self::QuorumDelivered { threshold } => { - RadrootsTransportSatisfactionPolicy::quorum_delivered(*threshold) - } - Self::RequiredAcceptedTargets { - target_fingerprints, - } => RadrootsTransportSatisfactionPolicy::required_targets( - RadrootsTransportSatisfactionClass::Accepted, - target_fingerprints.clone(), - )?, - Self::RequiredDeliveredTargets { - target_fingerprints, - } => RadrootsTransportSatisfactionPolicy::required_targets( - RadrootsTransportSatisfactionClass::Delivered, - target_fingerprints.clone(), - )?, - }) - } -} - -fn validate_satisfaction_threshold(threshold: u16) -> Result<(), RadrootsSdkError> { - if threshold == 0 { - return Err(RadrootsSdkError::InvalidRequest { - message: "satisfaction policy threshold must require at least one target".to_owned(), - }); - } - Ok(()) -} - -fn required_target_fingerprints<I, S>( - targets: I, -) -> Result<Vec<TargetFingerprint>, RadrootsSdkError> -where - I: IntoIterator<Item = S>, - S: AsRef<str>, -{ - let fingerprints = targets - .into_iter() - .map(|target| TargetFingerprint::parse(target.as_ref())) - .collect::<Result<Vec<_>, _>>()?; - RadrootsTransportSatisfactionPolicy::required_targets( - RadrootsTransportSatisfactionClass::Accepted, - fingerprints.clone(), - )?; - Ok(fingerprints) -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum NostrRelayUrlPolicy { - Public, - Localhost, -} - -impl NostrRelayUrlPolicy { - pub(crate) fn nostr_transport_policy(self) -> RadrootsRelayUrlPolicy { - match self { - Self::Public => RadrootsRelayUrlPolicy::Public, - Self::Localhost => RadrootsRelayUrlPolicy::Localhost, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum TargetPolicy { - Explicit(TargetSet), - DefaultProfile, - LocalOnly, - MeshScope(MeshScopeId), -} - -impl TargetPolicy { - pub fn explicit(targets: TargetSet) -> Self { - Self::Explicit(targets) - } - - pub fn try_nostr_relays<I, S>( - relays: I, - url_policy: NostrRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self::Explicit(TargetSet::nostr_relays(relays, url_policy)?)) - } - - pub fn default_profile() -> Self { - Self::DefaultProfile - } - - pub fn local_only() -> Self { - Self::LocalOnly - } - - pub fn mesh_scope(scope: MeshScopeId) -> Self { - Self::MeshScope(scope) - } -} - -impl serde::Serialize for TargetPolicy { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - match self { - Self::Explicit(targets) => { - let mut state = serializer.serialize_struct("TargetPolicy", 3)?; - state.serialize_field("kind", "explicit")?; - state.serialize_field("targets", targets.targets())?; - state.serialize_field("canonical_targets", targets.canonical_targets())?; - state.end() - } - Self::DefaultProfile => { - let mut state = serializer.serialize_struct("TargetPolicy", 1)?; - state.serialize_field("kind", "default_profile")?; - state.end() - } - Self::LocalOnly => { - let mut state = serializer.serialize_struct("TargetPolicy", 1)?; - state.serialize_field("kind", "local_only")?; - state.end() - } - Self::MeshScope(scope) => { - let mut state = serializer.serialize_struct("TargetPolicy", 2)?; - state.serialize_field("kind", "mesh_scope")?; - state.serialize_field("scope", scope)?; - state.end() - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(transparent)] -pub struct MeshScopeId(TargetScope); - -impl MeshScopeId { - pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsSdkError> { - Ok(Self(TargetScope::parse(raw)?)) - } - - pub fn local_reticulum() -> Self { - Self(TargetScope::parse(RADROOTS_RETICULUM_SCOPE_ID).expect("Reticulum scope")) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - pub(crate) fn transport_scope(&self) -> TargetScope { - self.0.clone() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TargetSet { - targets: Vec<Target>, - canonical_targets: Vec<String>, -} - -impl TargetSet { - pub fn nostr_relays<I, S>( - relays: I, - policy: NostrRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let mut targets = Vec::new(); - for relay in relays { - let normalized = normalized_nostr_relay_url(relay.as_ref(), policy)?; - targets.push(Target::new(TransportId::NOSTR, normalized)?); - } - Self::from_transport_targets(targets) - } - - pub fn transport_targets(targets: Vec<Target>) -> Result<Self, RadrootsSdkError> { - Self::from_transport_targets(targets) - } - - pub fn targets(&self) -> &[Target] { - self.targets.as_slice() - } - - pub fn canonical_targets(&self) -> &[String] { - self.canonical_targets.as_slice() - } - - pub fn transport_target_set(&self) -> Result<TransportTargetSet, RadrootsSdkError> { - Ok(TransportTargetSet::new(self.targets.clone())?) - } - - pub fn nostr_relay_urls(&self) -> Vec<String> { - self.targets - .iter() - .filter(|target| target.kind() == &TransportId::NOSTR) - .map(|target| target.uri().as_str().to_owned()) - .collect() - } - - pub fn into_targets(self) -> Vec<Target> { - self.targets - } - - pub fn len(&self) -> usize { - self.targets.len() - } - - pub fn is_empty(&self) -> bool { - self.targets.is_empty() - } - - fn from_transport_targets(targets: Vec<Target>) -> Result<Self, RadrootsSdkError> { - if targets.is_empty() { - return Err(RadrootsSdkError::empty_transport_targets( - "sdk transport target set", - )); - } - if targets.len() > SDK_TRANSPORT_TARGET_MAX_COUNT { - return Err(RadrootsSdkError::transport_target_limit_exceeded( - SDK_TRANSPORT_TARGET_MAX_COUNT, - targets.len(), - )); - } - for target in &targets { - if target.kind() == &TransportId::RETICULUM - && target.uri().as_str() != RADROOTS_RETICULUM_ENDPOINT_URI - { - return Err(RadrootsSdkError::InvalidRequest { - message: format!( - "Reticulum endpoint must be {RADROOTS_RETICULUM_ENDPOINT_URI}" - ), - }); - } - } - TransportTargetSet::new(targets.clone())?; - let canonical_targets = targets - .iter() - .map(|target| target.fingerprint().to_string()) - .collect::<BTreeSet<_>>() - .into_iter() - .collect(); - Ok(Self { - targets, - canonical_targets, - }) - } -} - -impl serde::Serialize for TargetSet { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("TargetSet", 2)?; - state.serialize_field("targets", self.targets())?; - state.serialize_field("canonical_targets", self.canonical_targets())?; - state.end() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct NostrProfile { - target_set: TargetSet, -} - -impl NostrProfile { - pub fn new<I, S>(relays: I, policy: NostrRelayUrlPolicy) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self { - target_set: TargetSet::nostr_relays(relays, policy)?, - }) - } - - pub fn target_set(&self) -> &TargetSet { - &self.target_set - } - - pub fn relay_urls(&self) -> Vec<String> { - self.target_set.nostr_relay_urls() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct ReticulumProfile { - endpoint_uri: String, - scope: MeshScopeId, - agent_endpoint: Option<ReticulumAgentEndpoint>, - behavior: ReticulumBehavior, -} - -impl ReticulumProfile { - pub fn deferred_until_implemented() -> Self { - Self { - endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), - scope: MeshScopeId::local_reticulum(), - agent_endpoint: None, - behavior: ReticulumBehavior::RejectDeliveryAttempts, - } - } - - pub fn with_behavior(mut self, behavior: ReticulumBehavior) -> Self { - self.behavior = behavior; - self - } - - pub fn endpoint_uri(&self) -> &str { - self.endpoint_uri.as_str() - } - - pub fn scope(&self) -> &MeshScopeId { - &self.scope - } - - pub fn agent_endpoint(&self) -> Option<&ReticulumAgentEndpoint> { - self.agent_endpoint.as_ref() - } - - pub fn with_agent_endpoint(mut self, agent_endpoint: ReticulumAgentEndpoint) -> Self { - self.agent_endpoint = Some(agent_endpoint); - self - } - - pub fn with_scope(mut self, scope: MeshScopeId) -> Self { - self.scope = scope; - self - } - - pub fn behavior(&self) -> ReticulumBehavior { - self.behavior - } - - pub fn target_set(&self) -> Result<TargetSet, RadrootsSdkError> { - if self.endpoint_uri.as_str() != RADROOTS_RETICULUM_ENDPOINT_URI { - return Err(radroots_transport::RadrootsTransportError::InvalidTargetUri.into()); - } - TargetSet::transport_targets(vec![Target::new_with_metadata( - TransportId::RETICULUM, - self.endpoint_uri.as_str(), - Some(self.scope.transport_scope()), - None, - )?]) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(transparent)] -pub struct ReticulumAgentEndpoint(String); - -impl ReticulumAgentEndpoint { - pub fn parse(raw: impl AsRef<str>) -> Result<Self, RadrootsSdkError> { - let uri = raw.as_ref(); - let Some(suffix) = uri.strip_prefix(RETICULUM_AGENT_ENDPOINT_PREFIX) else { - return Err(RadrootsSdkError::InvalidRequest { - message: "Reticulum agent endpoint is invalid".to_owned(), - }); - }; - if uri.is_empty() - || uri != uri.trim() - || suffix.is_empty() - || uri - .chars() - .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) - { - return Err(RadrootsSdkError::InvalidRequest { - message: "Reticulum agent endpoint is invalid".to_owned(), - }); - } - Ok(Self(uri.to_owned())) - } - - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -impl Default for ReticulumProfile { - fn default() -> Self { - Self::deferred_until_implemented() - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum ReticulumBehavior { - RejectDeliveryAttempts, - DeferDeliveryPlans, -} - -impl ReticulumBehavior { - pub fn as_str(self) -> &'static str { - match self { - Self::RejectDeliveryAttempts => "reject_delivery_attempts", - Self::DeferDeliveryPlans => "defer_delivery_plans", - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct MultiTargetProfile { - nostr: NostrProfile, - reticulum: ReticulumProfile, -} - -impl MultiTargetProfile { - pub fn new(nostr: NostrProfile, reticulum: ReticulumProfile) -> Self { - Self { nostr, reticulum } - } - - pub fn nostr(&self) -> &NostrProfile { - &self.nostr - } - - pub fn reticulum(&self) -> &ReticulumProfile { - &self.reticulum - } -} - -#[derive(Clone, Default, PartialEq, Eq)] -pub enum RadrootsdExecutionAuth { - #[default] - None, - BearerToken(String), -} - -impl core::fmt::Debug for RadrootsdExecutionAuth { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::None => f.write_str("None"), - Self::BearerToken(_) => f.write_str("BearerToken(<redacted>)"), - } - } -} - -impl serde::Serialize for RadrootsdExecutionAuth { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let mut state = serializer.serialize_struct("RadrootsdExecutionAuth", 1)?; - match self { - Self::None => state.serialize_field("kind", "none")?, - Self::BearerToken(_) => state.serialize_field("kind", "bearer_token")?, - } - state.end() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct RadrootsdExecutionProfile { - endpoint_url: String, - auth: RadrootsdExecutionAuth, -} - -impl RadrootsdExecutionProfile { - pub fn new(endpoint_url: impl Into<String>) -> Self { - Self { - endpoint_url: endpoint_url.into(), - auth: RadrootsdExecutionAuth::None, - } - } - - pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self { - self.auth = RadrootsdExecutionAuth::BearerToken(token.into()); - self - } - - pub fn endpoint_url(&self) -> &str { - self.endpoint_url.as_str() - } - - pub fn auth(&self) -> &RadrootsdExecutionAuth { - &self.auth - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case", tag = "kind")] -#[non_exhaustive] -pub enum TransportProfile { - #[default] - LocalOnly, - Nostr { - profile: NostrProfile, - }, - Reticulum { - profile: ReticulumProfile, - }, - MultiTarget { - profile: MultiTargetProfile, - }, -} - -impl TransportProfile { - pub fn local_only() -> Self { - Self::LocalOnly - } - - pub fn nostr(profile: NostrProfile) -> Self { - Self::Nostr { profile } - } - - pub fn reticulum(profile: ReticulumProfile) -> Self { - Self::Reticulum { profile } - } - - pub fn multi_target(profile: MultiTargetProfile) -> Self { - Self::MultiTarget { profile } - } - - pub(crate) fn transport_profile_id(&self) -> &'static str { - match self { - Self::LocalOnly => "local_only", - Self::Nostr { .. } => "nostr", - Self::Reticulum { .. } => "reticulum", - Self::MultiTarget { .. } => "multi_target", - } - } - - pub(crate) fn target_set(&self) -> Result<Option<TargetSet>, RadrootsSdkError> { - match self { - Self::LocalOnly => Ok(None), - Self::Nostr { profile } => Ok(Some(profile.target_set().clone())), - Self::Reticulum { profile } => Ok(Some(profile.target_set()?)), - Self::MultiTarget { profile } => { - let mut targets = profile.nostr().target_set().targets().to_vec(); - targets.extend(profile.reticulum().target_set()?.into_targets()); - Ok(Some(TargetSet::transport_targets(targets)?)) - } - } - } - - pub(crate) fn configured_transport_targets(&self) -> Result<Vec<Target>, RadrootsSdkError> { - Ok(self - .target_set()? - .map(TargetSet::into_targets) - .unwrap_or_default()) - } - - pub(crate) fn transport_statuses(&self) -> Vec<RadrootsTransportStatus> { - match self { - Self::LocalOnly => vec![ - RadrootsTransportStatus::new( - TransportId::LOCAL, - true, - RadrootsTransportImplementationState::Real, - false, - "local persistence only", - ) - .with_profile_id(self.transport_profile_id()), - ], - Self::Nostr { .. } => vec![nostr_transport_status(self.transport_profile_id())], - Self::Reticulum { profile } => { - vec![reticulum_transport_status( - self.transport_profile_id(), - profile.endpoint_uri(), - )] - } - Self::MultiTarget { profile } => vec![ - nostr_transport_status(self.transport_profile_id()), - reticulum_transport_status( - self.transport_profile_id(), - profile.reticulum().endpoint_uri(), - ), - ], - } - } - - pub(crate) fn configured_nostr_relay_urls(&self) -> Vec<String> { - match self { - Self::Nostr { profile } => profile.relay_urls(), - Self::MultiTarget { profile } => profile.nostr().relay_urls(), - Self::LocalOnly | Self::Reticulum { .. } => Vec::new(), - } - } -} - -fn nostr_transport_status(profile_id: &str) -> RadrootsTransportStatus { - RadrootsTransportStatus::new( - TransportId::NOSTR, - true, - RadrootsTransportImplementationState::Real, - true, - "ready", - ) - .with_profile_id(profile_id) -} - -fn reticulum_transport_status(profile_id: &str, endpoint_uri: &str) -> RadrootsTransportStatus { - RadrootsTransportStatus::new( - TransportId::RETICULUM, - true, - RadrootsTransportImplementationState::Real, - false, - RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, - ) - .with_profile_id(profile_id) - .with_endpoint_uri(endpoint_uri) - .with_maturity(RadrootsTransportCapabilityMaturity::Preview) - .with_availability(RadrootsTransportCapabilityAvailability::Unavailable) -} - -impl From<RadrootsTransportDeliveryReceipt> for TransportReceipt { - fn from(value: RadrootsTransportDeliveryReceipt) -> Self { - Self { - request_id: value.request_id().to_owned(), - target_set: value.target_set().clone(), - target_receipts: value.target_receipts().to_vec(), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct TransportReceipt { - request_id: String, - target_set: TransportTargetSet, - target_receipts: Vec<RadrootsTransportTargetReceipt>, -} - -impl TransportReceipt { - pub fn request_id(&self) -> &str { - self.request_id.as_str() - } - - pub fn target_set(&self) -> &TransportTargetSet { - &self.target_set - } - - pub fn target_receipts(&self) -> &[RadrootsTransportTargetReceipt] { - self.target_receipts.as_slice() - } - - pub fn satisfied_target_count( - &self, - satisfaction_class: RadrootsTransportSatisfactionClass, - ) -> usize { - self.target_receipts - .iter() - .filter(|receipt| receipt.status.counts_as_satisfied(satisfaction_class)) - .count() - } - - pub fn is_satisfied_by(&self, policy: &SatisfactionPolicy) -> Result<bool, RadrootsSdkError> { - let receipt = RadrootsTransportDeliveryReceipt::new( - self.request_id.clone(), - self.target_set.clone(), - self.target_receipts.clone(), - )?; - Ok(receipt.is_satisfied_by(&policy.transport_satisfaction_policy()?)?) - } -} - -fn normalized_nostr_relay_url( - value: &str, - policy: NostrRelayUrlPolicy, -) -> Result<String, RadrootsSdkError> { - let relay = RelayUrl::parse(value, policy.nostr_transport_policy())?; - Ok(relay.into_string()) -} - -#[cfg(test)] -#[path = "../tests/unit/transport_tests.rs"] -mod tests; +//! Transport capability composition. diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -0,0 +1,72 @@ +use std::collections::BTreeSet; + +const MANIFEST: &str = include_str!("../Cargo.toml"); +const ROOT: &str = include_str!("../src/lib.rs"); + +#[test] +fn manifest_has_final_identity_and_dependency_boundary() { + assert!(MANIFEST.contains("name = \"radroots_sdk\"")); + assert!(MANIFEST.contains("publish = false")); + assert!(MANIFEST.contains("version.workspace = true")); + assert!(MANIFEST.contains("name = \"package_boundary\"")); + + let dependencies = dependency_names(MANIFEST); + let expected = BTreeSet::from([ + "radroots_core", + "radroots_event", + "radroots_event_codec", + "radroots_geonames", + "radroots_identity", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_protocol", + "radroots_secrets", + "radroots_signing", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_sync", + "radroots_trade", + "radroots_transport", + "radroots_transport_nostr", + ]); + assert_eq!(dependencies, expected); +} + +#[test] +fn root_declares_exact_final_module_skeleton() { + let actual = ROOT + .lines() + .filter_map(|line| line.trim().strip_prefix("pub mod ")) + .map(|module| module.trim_end_matches(';')) + .collect::<BTreeSet<_>>(); + let expected = BTreeSet::from([ + "capability", + "client", + "diagnostics", + "error", + "farm", + "listing", + "signing", + "storage", + "sync", + "trade", + "transport", + ]); + assert_eq!(actual, expected); + assert!(!ROOT.contains("no_std")); +} + +fn dependency_names(manifest: &str) -> BTreeSet<&str> { + let dependencies = manifest + .split_once("[dependencies]") + .expect("dependency section") + .1 + .split_once("[[test]]") + .expect("test section") + .0; + dependencies + .lines() + .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim())) + .filter(|name| name.starts_with("radroots_")) + .collect() +}