lib

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

commit 5342e08dab1b595275b8641a91a5d6552891e366
parent 27c373feaa38c3b8a4a5cb2e65544f459baabc60
Author: triesap <tyson@radroots.org>
Date:   Fri,  7 Aug 2026 15:30:36 +0000

feat(mobile): enforce opaque host signing

- replace local secret slots and duplicate social authoring with focused host signer operations
- domain-separate BUD-11 HTTP authorization from durable relay event plans
- revalidate signer output against exact request deadline cancellation and signature before commit
- refresh public APIs docs and tests across consolidated mobile surfaces

Diffstat:
MCargo.lock | 4++++
Mcrates/event_codec/README.md | 4++++
Mcrates/event_codec/src/authoring/mod.rs | 206++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/event_codec/tests/package_boundary.rs | 2++
Mcrates/mobile_core/Cargo.toml | 4+++-
Mcrates/mobile_core/src/runtime/builder.rs | 23+++++++++++++++++++++--
Dcrates/mobile_core/src/runtime/key_management.rs | 222-------------------------------------------------------------------------------
Mcrates/mobile_core/src/runtime/mod.rs | 41++++++++++++++++++-----------------------
Dcrates/mobile_core/src/runtime/nostr.rs | 240-------------------------------------------------------------------------------
Mcrates/mobile_core/src/runtime/product_surface/outbox.rs | 170++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/mobile_core/tests/durable_runtime.rs | 18------------------
Mcrates/mobile_core/tests/package_boundary.rs | 27+++++++++++++++++++++++----
Mcrates/mobile_ffi/src/remote.rs | 77-----------------------------------------------------------------------------
Mcrates/mobile_ffi/src/runtime.rs | 143-------------------------------------------------------------------------------
Mcrates/mobile_ffi/tests/runtime_delegation.rs | 95-------------------------------------------------------------------------------
Mcrates/nostr/README.md | 9+++++----
Mcrates/nostr/src/blossom.rs | 31+++++++++++++++++++++++++++++++
Mcrates/nostr/src/plan_signing.rs | 41+++++++++++++++++++++++++++++++++++++----
Mcrates/nostr/src/signing.rs | 6+++---
Mcrates/nostr/tests/blossom_conformance.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/nostr/tests/package_boundary.rs | 9+++++++++
Mcrates/radroots/Cargo.toml | 3++-
Mcrates/radroots/README.md | 3++-
Mcrates/radroots/src/signing.rs | 7++++++-
Mcrates/sdk/Cargo.toml | 5++++-
Mcrates/sdk/README.md | 69++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/sdk/src/client.rs | 864++-----------------------------------------------------------------------------
Mcrates/sdk/src/error.rs | 46----------------------------------------------
Mcrates/sdk/src/signing.rs | 502+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/sdk/tests/package_boundary.rs | 2++
Mcrates/sdk/tests/public_api.rs | 28++++++++--------------------
Mcrates/signing/Cargo.toml | 3+++
Mcrates/signing/README.md | 9+++++++++
Mcrates/signing/src/lib.rs | 2+-
Mcrates/signing/src/receipt.rs | 13++++++-------
Mcrates/signing/src/request.rs | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/signing/tests/authored_signing.rs | 149++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/signing/tests/package_boundary.rs | 8+++++---
Mcrates/sync/src/push.rs | 31+++++++++++++++++++++++++++++--
Mcrates/sync/tests/push_enqueue.rs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mdocs/api/README.md | 2++
Adocs/api/radroots.txt | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/api/radroots_event_codec.txt | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mdocs/api/radroots_nostr.txt | 11+++++++++--
Adocs/api/radroots_sdk.txt | 518+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/api/radroots_signing.txt | 34++++++++++++++++++++++++++++++----
46 files changed, 2200 insertions(+), 2122 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3572,6 +3572,8 @@ dependencies = [ "radroots_event", "radroots_event_codec", "radroots_identity", + "radroots_nostr", + "radroots_protocol", "radroots_sdk", "radroots_signing", "radroots_storage", @@ -3783,6 +3785,7 @@ name = "radroots_sdk" version = "0.1.0-alpha" dependencies = [ "nostr 0.44.7", + "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", @@ -3850,6 +3853,7 @@ version = "0.1.0-alpha" dependencies = [ "hex", "nostr 0.44.7", + "radroots_blossom", "radroots_event", "radroots_event_codec", "radroots_identity", diff --git a/crates/event_codec/README.md b/crates/event_codec/README.md @@ -16,6 +16,7 @@ New code should enter through these modules: | Module | Responsibility | | --- | --- | +| `authoring` | Freeze exact registry-authored event plans and distinct HTTP-only Blossom authorization plans with semantic digests. | | `canonical` | Compute canonical NIP-01 preimages and event identifiers without asserting trust. | | `decode` | Parse bounded wire data and domain projections without silently verifying later stages. | | `encode` | Produce deterministic JSON, tags, and unsigned wire parts from validated native inputs. | @@ -85,6 +86,9 @@ guarantees. declared ID. - Domain encoders accept checked `radroots_event` inputs and emit unsigned wire parts. Signing and publication belong to the owning runtime. +- `authoring::BlossomAuthorizationPlan` binds a strict BUD-11 upload claim to + an expected author and event ID under a domain-separated digest. Its + distinct type cannot be passed to relay push APIs. - Manifest JSON and digests are generated from versioned contract authority; a manifest feature does not grant storage or publication authority. diff --git a/crates/event_codec/src/authoring/mod.rs b/crates/event_codec/src/authoring/mod.rs @@ -6,12 +6,21 @@ use alloc::{borrow::ToOwned, string::String, vec::Vec}; use std::{borrow::ToOwned, string::String, vec::Vec}; use core::fmt; -use radroots_event::{GenericEventDraft, contract::ContractKey, draft::DraftError, id::EventId}; +use radroots_blossom::authorization::AuthoredUploadClaim; +use radroots_event::{ + GenericEventDraft, + contract::ContractKey, + draft::DraftError, + id::EventId, + wire::{CanonicalEventIdError, compute_canonical_nip01_event_id}, +}; use radroots_identity::PublicKey; use sha2::{Digest, Sha256}; pub const PLAN_WIRE_VERSION_V1: u32 = 1; const PLAN_DIGEST_DOMAIN: &[u8] = b"radroots.authored_event_plan.v1"; +const BLOSSOM_AUTHORIZATION_PLAN_DIGEST_DOMAIN: &[u8] = + b"radroots.blossom_upload_authorization_plan.v1"; mod typed; mod wire; @@ -135,6 +144,94 @@ pub struct AuthoredEventPlan { digest: PlanDigest, } +/// Exact BUD-11 upload-authorization event plan for HTTP use only. +/// +/// This type is deliberately distinct from [`AuthoredEventPlan`]. It cannot be +/// accepted by relay push APIs and therefore cannot accidentally publish a +/// short-lived Blossom authorization token as a Nostr event. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlossomAuthorizationPlan { + author: PublicKey, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + expected_event_id: EventId, + digest: PlanDigest, +} + +impl BlossomAuthorizationPlan { + /// Binds one validated upload claim to the exact expected signer identity. + pub fn for_upload( + claim: &AuthoredUploadClaim, + author: PublicKey, + ) -> Result<Self, CanonicalEventIdError> { + let wire = claim.wire_parts(); + let kind = u32::from(wire.kind()); + let tags = wire.tags().to_vec(); + let content = wire.content().to_owned(); + let expected_event_id = compute_canonical_nip01_event_id( + author.to_hex().as_str(), + wire.created_at(), + kind, + &tags, + content.as_str(), + )?; + let digest = compute_blossom_authorization_plan_digest( + &author, + wire.created_at(), + kind, + &tags, + content.as_str(), + &expected_event_id, + ); + Ok(Self { + author, + created_at: wire.created_at(), + kind, + tags, + content, + expected_event_id, + digest, + }) + } + + #[must_use] + pub const fn author(&self) -> &PublicKey { + &self.author + } + + #[must_use] + pub const fn created_at(&self) -> u64 { + self.created_at + } + + #[must_use] + pub const fn kind(&self) -> u32 { + self.kind + } + + #[must_use] + pub fn tags(&self) -> &[Vec<String>] { + &self.tags + } + + #[must_use] + pub fn content(&self) -> &str { + self.content.as_str() + } + + #[must_use] + pub const fn expected_event_id(&self) -> &EventId { + &self.expected_event_id + } + + #[must_use] + pub const fn digest(&self) -> PlanDigest { + self.digest + } +} + impl AuthoredEventPlan { /// Converts the generic-only event input into its immutable authored plan. pub fn from_generic(draft: GenericEventDraft) -> Result<Self, DraftError> { @@ -219,6 +316,31 @@ fn compute_plan_digest( encoder.finish() } +fn compute_blossom_authorization_plan_digest( + author: &PublicKey, + created_at: u64, + kind: u32, + tags: &[Vec<String>], + content: &str, + expected_event_id: &EventId, +) -> PlanDigest { + let mut encoder = DigestEncoder::new(); + encoder.bytes(BLOSSOM_AUTHORIZATION_PLAN_DIGEST_DOMAIN); + encoder.bytes(author.as_bytes()); + encoder.u64(created_at); + encoder.u32(kind); + encoder.u32(tags.len() as u32); + for tag in tags { + encoder.u32(tag.len() as u32); + for element in tag { + encoder.bytes(element.as_bytes()); + } + } + encoder.bytes(content.as_bytes()); + encoder.bytes(expected_event_id.as_bytes()); + encoder.finish() +} + struct DigestEncoder(Sha256); impl DigestEncoder { @@ -247,6 +369,10 @@ impl DigestEncoder { #[cfg(test)] mod tests { use super::*; + use radroots_blossom::{ + Sha256 as BlossomSha256, + authorization::{AuthorizationContent, ServerDomain}, + }; use radroots_event::envelope::kind::KIND_GEOCHAT; const ALICE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -320,4 +446,82 @@ mod tests { assert_ne!(variant.digest(), base.digest()); } } + + fn blossom_plan( + author: &str, + created_at: u64, + content: &str, + server: &str, + payload: &[u8], + ) -> BlossomAuthorizationPlan { + let claim = AuthoredUploadClaim::new( + AuthorizationContent::parse(content).expect("content"), + ServerDomain::parse(server).expect("server"), + BlossomSha256::digest(payload), + created_at, + 60, + ) + .expect("upload claim"); + BlossomAuthorizationPlan::for_upload(&claim, PublicKey::from_hex(author).expect("author")) + .expect("authorization plan") + } + + #[test] + fn blossom_upload_plan_binds_every_http_authorization_field() { + let base = blossom_plan( + ALICE, + 1_700_000_000, + "Upload exact image", + "media.example", + b"exact-image", + ); + assert_eq!(base.kind(), 24_242); + assert_eq!(base.author().to_hex(), ALICE); + assert_eq!(base.created_at(), 1_700_000_000); + assert_eq!(base.content(), "Upload exact image"); + assert_eq!(base.tags().len(), 4); + assert_eq!(base.digest().to_hex().len(), 64); + + let variants = [ + blossom_plan( + BOB, + 1_700_000_000, + "Upload exact image", + "media.example", + b"exact-image", + ), + blossom_plan( + ALICE, + 1_700_000_001, + "Upload exact image", + "media.example", + b"exact-image", + ), + blossom_plan( + ALICE, + 1_700_000_000, + "Upload another image", + "media.example", + b"exact-image", + ), + blossom_plan( + ALICE, + 1_700_000_000, + "Upload exact image", + "uploads.example", + b"exact-image", + ), + blossom_plan( + ALICE, + 1_700_000_000, + "Upload exact image", + "media.example", + b"different-image", + ), + ]; + for variant in variants { + assert_ne!(variant.expected_event_id(), base.expected_event_id()); + assert_ne!(variant.digest(), base.digest()); + } + } } diff --git a/crates/event_codec/tests/package_boundary.rs b/crates/event_codec/tests/package_boundary.rs @@ -196,12 +196,14 @@ fn package_documentation_and_reviewed_api_baseline_are_complete() { assert!(PUBLIC_API.starts_with("pub mod radroots_event_codec\n")); for item in [ "pub mod radroots_event_codec::admission", + "pub mod radroots_event_codec::authoring", "pub mod radroots_event_codec::canonical", "pub mod radroots_event_codec::decode", "pub mod radroots_event_codec::encode", "pub mod radroots_event_codec::manifest", "pub mod radroots_event_codec::verify", "pub use radroots_event_codec::VerificationError", + "pub struct radroots_event_codec::authoring::BlossomAuthorizationPlan", ] { assert!(PUBLIC_API.contains(item), "API baseline is missing {item}"); } diff --git a/crates/mobile_core/Cargo.toml b/crates/mobile_core/Cargo.toml @@ -21,7 +21,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [features] default = [] mobile-social = [ - "radroots_sdk/local-signing", + "radroots_sdk/blossom", "radroots_sdk/nostr", "radroots_sdk/sync", ] @@ -35,6 +35,7 @@ radroots_sdk = { workspace = true, features = ["sqlite"] } radroots_event = { workspace = true, default-features = false, features = ["std"] } radroots_event_codec = { workspace = true, default-features = false, features = ["json", "std"] } radroots_identity = { workspace = true, default-features = false, features = ["std"] } +radroots_protocol = { workspace = true, default-features = false, features = ["std"] } radroots_signing = { workspace = true, default-features = false, features = ["std"] } radroots_storage = { workspace = true, default-features = false } radroots_sync = { workspace = true, default-features = false } @@ -48,6 +49,7 @@ thiserror = { workspace = true } [dev-dependencies] nostr = { workspace = true, features = ["std"] } +radroots_nostr = { workspace = true, features = ["blossom", "signing"] } radroots_sdk = { workspace = true, features = ["memory", "sqlite"] } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/crates/mobile_core/src/runtime/builder.rs b/crates/mobile_core/src/runtime/builder.rs @@ -4,12 +4,26 @@ use crate::{RadrootsAppError, RadrootsRuntime}; /// Host-owned construction boundary for the shared SDK-backed runtime. pub struct RuntimeBuilder { store: MobileUserStoreConfig, + #[cfg(feature = "mobile-social")] + signer: Option<std::sync::Arc<dyn radroots_signing::Signer>>, } impl RuntimeBuilder { #[must_use] pub const fn new(store: MobileUserStoreConfig) -> Self { - Self { store } + Self { + store, + #[cfg(feature = "mobile-social")] + signer: None, + } + } + + /// Installs one opaque host signer without transferring secret material. + #[cfg(feature = "mobile-social")] + #[must_use] + pub fn signer(mut self, signer: std::sync::Arc<dyn radroots_signing::Signer>) -> Self { + self.signer = Some(signer); + self } /// Opens the exact authenticated user's durable SQLite store. @@ -22,7 +36,12 @@ impl RuntimeBuilder { let builder = radroots_sdk::ClientBuilder::sqlite(options) .await .map_err(RadrootsAppError::from_sdk)?; - RadrootsRuntime::from_client_builder(builder, Some(self.store.public_key())) + RadrootsRuntime::from_client_builder( + builder, + Some(self.store.public_key()), + #[cfg(feature = "mobile-social")] + self.signer, + ) } } diff --git a/crates/mobile_core/src/runtime/key_management.rs b/crates/mobile_core/src/runtime/key_management.rs @@ -1,222 +0,0 @@ -//! Host-custodied mobile identity presentation over the SDK signer slot. - -use super::RadrootsRuntime; -use crate::RadrootsAppError; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrIdentityRecord { - pub id: String, - pub public_key_hex: String, - pub public_key_npub: String, - pub label: Option<String>, - pub is_selected: bool, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrIdentitySnapshot { - pub has_selected_signing_identity: bool, - pub selected_identity_id: Option<String>, - pub selected_npub: Option<String>, - pub identities: Vec<NostrIdentityRecord>, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrHostCustodyIdentity { - pub id: String, - pub public_key_hex: String, - pub public_key_npub: String, -} - -fn host_identity(identity: &radroots_sdk::signing::LocalIdentity) -> NostrHostCustodyIdentity { - let public_key_hex = identity.public_key_hex(); - NostrHostCustodyIdentity { - id: public_key_hex.clone(), - public_key_hex, - public_key_npub: identity.npub().to_owned(), - } -} - -fn identity_record( - identity: &radroots_sdk::signing::LocalIdentity, - label: Option<String>, -) -> NostrIdentityRecord { - let identity = host_identity(identity); - NostrIdentityRecord { - id: identity.id, - public_key_hex: identity.public_key_hex, - public_key_npub: identity.public_key_npub, - label, - is_selected: true, - } -} - -impl RadrootsRuntime { - pub fn nostr_identity_has_selected_signing_identity(&self) -> bool { - self.signing_slot.identity().is_some() - } - - pub fn nostr_identity_selected_npub(&self) -> Option<String> { - self.signing_slot - .identity() - .map(|identity| identity.npub().to_owned()) - } - - pub fn nostr_identity_list(&self) -> Result<Vec<NostrIdentityRecord>, RadrootsAppError> { - let Some(identity) = self.signing_slot.identity() else { - return Ok(Vec::new()); - }; - Ok(vec![identity_record(&identity, self.identity_label())]) - } - - pub fn nostr_identity_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { - Ok(self - .nostr_identity_list()? - .into_iter() - .map(|identity| identity.id) - .collect()) - } - - pub fn nostr_identity_snapshot(&self) -> Result<NostrIdentitySnapshot, RadrootsAppError> { - let identities = self.nostr_identity_list()?; - let selected = identities.first(); - Ok(NostrIdentitySnapshot { - has_selected_signing_identity: selected.is_some(), - selected_identity_id: selected.map(|identity| identity.id.clone()), - selected_npub: selected.map(|identity| identity.public_key_npub.clone()), - identities, - }) - } - - pub fn nostr_identity_validate_host_custody_secret( - &self, - secret_key: String, - ) -> Result<NostrHostCustodyIdentity, RadrootsAppError> { - let slot = radroots_sdk::signing::Slot::new(); - let identity = slot - .install(secret_key.as_str()) - .map_err(|_| RadrootsAppError::runtime("identity secret is invalid"))?; - slot.clear(); - Ok(host_identity(&identity)) - } - - pub fn nostr_identity_restore_host_custody_secret( - &self, - secret_key: String, - label: Option<String>, - make_selected: bool, - ) -> Result<NostrIdentityRecord, RadrootsAppError> { - if !make_selected { - let identity = self.nostr_identity_validate_host_custody_secret(secret_key)?; - return Ok(NostrIdentityRecord { - id: identity.id, - public_key_hex: identity.public_key_hex, - public_key_npub: identity.public_key_npub, - label, - is_selected: false, - }); - } - let identity = self - .signing_slot - .install(secret_key.as_str()) - .map_err(|_| RadrootsAppError::runtime("identity secret is invalid"))?; - if self - .store_public_key - .is_some_and(|expected| expected.to_hex() != identity.public_key_hex()) - { - self.signing_slot.clear(); - return Err(RadrootsAppError::runtime( - "identity does not match the authenticated user store", - )); - } - self.set_identity_label(label.clone())?; - Ok(identity_record(&identity, label)) - } - - pub fn nostr_identity_select(&self, identity_id: String) -> Result<(), RadrootsAppError> { - let current = self - .signing_slot - .identity() - .ok_or_else(|| RadrootsAppError::runtime("identity is not installed"))?; - if current.public_key_hex() != identity_id { - return Err(RadrootsAppError::runtime("identity is not installed")); - } - Ok(()) - } - - pub fn nostr_identity_remove(&self, identity_id: String) -> Result<(), RadrootsAppError> { - if self - .signing_slot - .identity() - .is_some_and(|identity| identity.public_key_hex() == identity_id) - { - self.signing_slot.clear(); - self.set_identity_label(None)?; - } - Ok(()) - } - - pub fn nostr_identity_lock_host_custody_runtime(&self) -> Result<(), RadrootsAppError> { - self.signing_slot.clear(); - self.set_identity_label(None) - } - - pub fn nostr_identity_reset_host_custody_runtime(&self) -> Result<(), RadrootsAppError> { - self.nostr_identity_lock_host_custody_runtime() - } - - fn identity_label(&self) -> Option<String> { - self.identity_label - .read() - .ok() - .and_then(|label| label.clone()) - } - - fn set_identity_label(&self, label: Option<String>) -> Result<(), RadrootsAppError> { - let mut current = self - .identity_label - .write() - .map_err(|_| RadrootsAppError::runtime("identity label state is unavailable"))?; - *current = label; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const SECRET: &str = "0000000000000000000000000000000000000000000000000000000000000001"; - - #[test] - fn validation_does_not_select_and_restore_is_single_slot() { - let runtime = RadrootsRuntime::test_memory().expect("runtime"); - let validated = runtime - .nostr_identity_validate_host_custody_secret(SECRET.to_owned()) - .expect("valid secret"); - assert!(!runtime.nostr_identity_has_selected_signing_identity()); - - let staged = runtime - .nostr_identity_restore_host_custody_secret( - SECRET.to_owned(), - Some("staged".to_owned()), - false, - ) - .expect("staged"); - assert_eq!(staged.id, validated.id); - assert!(!staged.is_selected); - - let selected = runtime - .nostr_identity_restore_host_custody_secret( - SECRET.to_owned(), - Some("selected".to_owned()), - true, - ) - .expect("selected"); - assert!(selected.is_selected); - assert_eq!(runtime.nostr_identity_list().expect("list"), vec![selected]); - runtime - .nostr_identity_lock_host_custody_runtime() - .expect("lock"); - assert!(runtime.nostr_identity_list().expect("list").is_empty()); - } -} diff --git a/crates/mobile_core/src/runtime/mod.rs b/crates/mobile_core/src/runtime/mod.rs @@ -1,10 +1,6 @@ pub mod app_info; pub mod builder; pub mod info; -#[cfg(feature = "mobile-social")] -pub mod key_management; -#[cfg(feature = "mobile-social")] -pub mod nostr; pub mod product_surface; pub mod sdk; pub mod store; @@ -25,12 +21,6 @@ use crate::RadrootsAppError; pub struct RadrootsRuntime { pub(crate) client: Client, - #[cfg(feature = "mobile-social")] - pub(crate) signing_slot: radroots_sdk::signing::Slot, - #[cfg(feature = "mobile-social")] - pub(crate) nostr_slot: radroots_sdk::transport::NostrSlot, - #[cfg(feature = "mobile-social")] - pub(crate) identity_label: RwLock<Option<String>>, pub(crate) started_unix_ms: i64, pub(crate) shutting_down: AtomicBool, pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, @@ -41,28 +31,28 @@ impl RadrootsRuntime { pub(crate) fn from_client_builder( builder: ClientBuilder, store_public_key: Option<PublicKey>, + #[cfg(feature = "mobile-social")] signer: Option< + std::sync::Arc<dyn radroots_signing::Signer>, + >, ) -> Result<Self, RadrootsAppError> { #[cfg(feature = "mobile-social")] - let signing_slot = radroots_sdk::signing::Slot::new(); - #[cfg(feature = "mobile-social")] let nostr_slot = radroots_sdk::transport::NostrSlot::new( radroots_sdk::transport::RelayUrlPolicy::Public, ); #[cfg(feature = "mobile-social")] - let builder = builder - .signing(radroots_sdk::signing::Provider::slot(signing_slot.clone())) - .nostr(nostr_slot.clone()) - .host_sync(radroots_sdk::sync::HostPolicy::standard()); + let builder = { + let builder = builder + .nostr(nostr_slot.clone()) + .host_sync(radroots_sdk::sync::HostPolicy::standard()); + match signer { + Some(signer) => builder.signing(radroots_sdk::signing::Provider::host(signer)), + None => builder, + } + }; let client = builder.build().map_err(RadrootsAppError::from_sdk)?; Ok(Self { client, - #[cfg(feature = "mobile-social")] - signing_slot, - #[cfg(feature = "mobile-social")] - nostr_slot, - #[cfg(feature = "mobile-social")] - identity_label: RwLock::new(None), started_unix_ms: Utc::now().timestamp_millis(), shutting_down: AtomicBool::new(false), platform_app: RwLock::new(None), @@ -72,7 +62,12 @@ impl RadrootsRuntime { #[cfg(test)] pub(crate) fn test_memory() -> Result<Self, RadrootsAppError> { - Self::from_client_builder(ClientBuilder::memory_default(), None) + Self::from_client_builder( + ClientBuilder::memory_default(), + None, + #[cfg(feature = "mobile-social")] + None, + ) } /// Closes SDK resources asynchronously across every runtime reference. diff --git a/crates/mobile_core/src/runtime/nostr.rs b/crates/mobile_core/src/runtime/nostr.rs @@ -1,240 +0,0 @@ -//! Bounded mobile Nostr presentation over shared SDK operations. - -use super::RadrootsRuntime; -use crate::RadrootsAppError; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NostrLight { - Red, - Yellow, - Green, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrConnectionStatus { - pub light: NostrLight, - pub configured: bool, - pub source_available: bool, - pub sink_available: bool, - pub last_error: Option<String>, -} - -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct NostrProfile { - pub name: Option<String>, - pub display_name: Option<String>, - pub nip05: Option<String>, - pub about: Option<String>, - pub website: Option<String>, - pub picture: Option<String>, - pub banner: Option<String>, - pub lud06: Option<String>, - pub lud16: Option<String>, - pub bot: Option<String>, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrProfileEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub profile: NostrProfile, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrPost { - pub content: String, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct NostrPostEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub post: NostrPost, -} - -fn map_profile(event: radroots_sdk::client::ProfileEvent) -> NostrProfileEventMetadata { - NostrProfileEventMetadata { - id: event.event_id().to_owned(), - author: event.author().to_owned(), - published_at: event.created_at(), - profile: NostrProfile { - name: event.name().map(str::to_owned), - display_name: event.display_name().map(str::to_owned), - nip05: event.nip05().map(str::to_owned), - about: event.about().map(str::to_owned), - website: None, - picture: event.picture().map(str::to_owned), - banner: event.banner().map(str::to_owned), - lud06: None, - lud16: None, - bot: event.bot().map(|value| value.to_string()), - }, - } -} - -fn map_post(event: radroots_sdk::client::PostEvent) -> NostrPostEventMetadata { - NostrPostEventMetadata { - id: event.event_id().to_owned(), - author: event.author().to_owned(), - published_at: event.created_at(), - post: NostrPost { - content: event.content().to_owned(), - }, - } -} - -impl RadrootsRuntime { - pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> { - self.nostr_slot - .configure(relays) - .map_err(RadrootsAppError::from_sdk) - } - - /// Validates readiness; relay connections remain operation-scoped. - pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> { - if self.signing_slot.identity().is_none() { - return Err(RadrootsAppError::runtime("identity is not installed")); - } - if self.nostr_slot.targets().is_none() { - return Err(RadrootsAppError::runtime( - "relay selection is not configured", - )); - } - Ok(()) - } - - pub async fn nostr_connection_status(&self) -> Result<NostrConnectionStatus, RadrootsAppError> { - let social = self.client.social().map_err(RadrootsAppError::from_sdk)?; - let health = social - .transport_health() - .await - .map_err(RadrootsAppError::from_sdk)?; - let light = if health.is_source_available() && health.is_sink_available() { - NostrLight::Green - } else if health.is_configured() { - NostrLight::Yellow - } else { - NostrLight::Red - }; - Ok(NostrConnectionStatus { - light, - configured: health.is_configured(), - source_available: health.is_source_available(), - sink_available: health.is_sink_available(), - last_error: None, - }) - } - - pub async fn nostr_profile_for_self( - &self, - ) -> Result<Option<NostrProfileEventMetadata>, RadrootsAppError> { - self.client - .social() - .map_err(RadrootsAppError::from_sdk)? - .fetch_profile_for_signer() - .await - .map(|profile| profile.map(map_profile)) - .map_err(RadrootsAppError::from_sdk) - } - - pub async fn nostr_post_profile( - &self, - name: Option<String>, - display_name: Option<String>, - nip05: Option<String>, - about: Option<String>, - ) -> Result<String, RadrootsAppError> { - let name = name - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| RadrootsAppError::runtime("profile name is required"))?; - let mut draft = radroots_sdk::client::ProfileDraft::new(name); - if let Some(value) = display_name.filter(|value| !value.is_empty()) { - draft = draft.with_display_name(value); - } - if let Some(value) = nip05.filter(|value| !value.is_empty()) { - draft = draft.with_nip05(value); - } - if let Some(value) = about.filter(|value| !value.is_empty()) { - draft = draft.with_about(value); - } - self.client - .social() - .map_err(RadrootsAppError::from_sdk)? - .publish_profile(draft) - .await - .map(|receipt| receipt.event_id().to_owned()) - .map_err(RadrootsAppError::from_sdk) - } - - pub async fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> { - self.client - .social() - .map_err(RadrootsAppError::from_sdk)? - .publish_text(content) - .await - .map(|receipt| receipt.event_id().to_owned()) - .map_err(RadrootsAppError::from_sdk) - } - - pub async fn nostr_fetch_text_notes( - &self, - limit: u16, - since_unix: Option<u64>, - ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> { - self.client - .social() - .map_err(RadrootsAppError::from_sdk)? - .fetch_posts(limit, since_unix) - .await - .map(|events| events.into_iter().map(map_post).collect()) - .map_err(RadrootsAppError::from_sdk) - } - - pub async fn nostr_post_reply( - &self, - parent_event_id_hex: String, - parent_author_hex: String, - content: String, - root_event_id_hex: Option<String>, - ) -> Result<String, RadrootsAppError> { - if root_event_id_hex - .as_deref() - .is_some_and(|root| root != parent_event_id_hex.as_str()) - { - return Err(RadrootsAppError::unsupported( - "nested reply author context is required", - )); - } - self.client - .social() - .map_err(RadrootsAppError::from_sdk)? - .publish_reply( - content, - parent_event_id_hex.as_str(), - parent_author_hex.as_str(), - None, - ) - .await - .map(|receipt| receipt.event_id().to_owned()) - .map_err(RadrootsAppError::from_sdk) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn relay_configuration_is_explicit_and_status_is_categorical() { - let runtime = RadrootsRuntime::test_memory().expect("runtime"); - let initial = runtime - .nostr_connection_status() - .await - .expect("initial status"); - assert_eq!(initial.light, NostrLight::Red); - assert!(!initial.configured); - assert!(runtime.nostr_set_default_relays(Vec::new()).is_err()); - } -} diff --git a/crates/mobile_core/src/runtime/product_surface/outbox.rs b/crates/mobile_core/src/runtime/product_surface/outbox.rs @@ -1,10 +1,14 @@ use std::collections::BTreeSet; -use radroots_blossom::{BlobUrl, MediaType}; +use radroots_blossom::{BlobUrl, MediaType, authorization::AuthoredUploadClaim}; use radroots_event::contract::AuthorRole; use radroots_event_codec::authoring::PlanWireV1; use radroots_identity::PublicKey; -use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy}; +use radroots_signing::{ + Actor, AuthoredArtifactId, SigningIntentId, SigningOperationId, + actor::ActorSource, + request::{CancellationPolicy, SignPolicy}, +}; use radroots_storage::{ authored::{AdmissionState, SigningState}, authored_delivery::{AuthoredDeliveryState, DeliveryAttemptOutcome}, @@ -140,6 +144,15 @@ pub enum Phase1CancellationPolicy { LocalCooperative, } +impl Phase1CancellationPolicy { + const fn signing(self) -> CancellationPolicy { + match self { + Self::PreservePublishedRequest => CancellationPolicy::PreservePublishedRequest, + Self::LocalCooperative => CancellationPolicy::LocalCooperative, + } + } +} + /// Exact relay and deadline intent frozen before an operation is prepared. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields)] @@ -614,6 +627,79 @@ impl RadrootsRuntime { } } + /// Invokes the configured opaque host signer for one durably queued draft. + /// + /// The canonical sync engine verifies author, event ID, exact fields, + /// signature, deadline, cancellation, and operation binding before the + /// signed artifact can be persisted. Delivery remains a separate phase. + pub async fn phase1_sign_queued_draft( + &self, + draft_id: [u8; 16], + expected_revision: u64, + ) -> Result<Phase1DraftStatus, Phase1DraftError> { + let draft_id = + AuthoredDraftId::new(draft_id).map_err(|_| Phase1DraftError::InvalidDraft)?; + let expected = AuthoredDraftRevision::new(expected_revision) + .map_err(|_| Phase1DraftError::RevisionConflict)?; + let head = self + .storage()? + .authored_draft_head(draft_id) + .await + .map_err(|_| Phase1DraftError::Storage)? + .ok_or(Phase1DraftError::NotFound)?; + if head.revision() != expected || head.stage() != AuthoredDraftStage::Queued { + return Err(Phase1DraftError::RevisionConflict); + } + self.sync()? + .sign_prepared(push_request(&head)?) + .await + .map_err(|_| Phase1DraftError::Operation)?; + self.draft_status_from(head).await + } + + /// Signs one short-lived BUD-11 upload credential for HTTP use only. + /// + /// The returned value is not persisted and its distinct plan type cannot + /// enter the relay push pipeline. + #[allow(clippy::too_many_arguments)] + pub async fn phase1_authorize_blossom_upload( + &self, + operation_id: [u8; 16], + artifact_id: [u8; 16], + claim: AuthoredUploadClaim, + deadline_unix_ms: u64, + cancellation: Phase1CancellationPolicy, + ) -> Result<radroots_sdk::signing::AuthorizationHeader, Phase1DraftError> { + let public_key = self + .store_public_key + .ok_or(Phase1DraftError::IdentityUnavailable)?; + let actor = Actor::new(public_key, ActorSource::ExplicitPublicKey, AuthorRole::ALL) + .map_err(|_| Phase1DraftError::IdentityUnavailable)?; + let plan = radroots_sdk::signing::BlossomAuthorizationPlan::for_upload(&claim, public_key) + .map_err(|_| Phase1DraftError::InvalidMedia)?; + let operation_id = + SigningOperationId::new(operation_id).map_err(|_| Phase1DraftError::InvalidDraft)?; + let artifact_id = + AuthoredArtifactId::new(artifact_id).map_err(|_| Phase1DraftError::InvalidDraft)?; + let policy = SignPolicy::new(deadline_unix_ms, cancellation.signing()) + .map_err(|_| Phase1DraftError::InvalidDraft)?; + let request = radroots_sdk::signing::blossom_upload_request( + radroots_protocol::runtime::v1::OperationId::SyncPush, + SigningIntentId::new(operation_id, artifact_id), + actor, + plan, + policy, + ) + .map_err(|_| Phase1DraftError::Operation)?; + self.client + .signing() + .map_err(|_| Phase1DraftError::OperationUnavailable)? + .ok_or(Phase1DraftError::OperationUnavailable)? + .authorize_blossom_upload(request) + .await + .map_err(|_| Phase1DraftError::Operation) + } + /// Returns durable draft state composed with canonical authored-operation state. pub async fn phase1_draft_status( &self, @@ -1085,12 +1171,27 @@ mod tests { }; use radroots_sdk::ClientBuilder; - const AUTHOR: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const AUTHOR: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; + const SECRET: &str = "0000000000000000000000000000000000000000000000000000000000000001"; fn runtime() -> RadrootsRuntime { RadrootsRuntime::from_client_builder( ClientBuilder::memory_default(), Some(PublicKey::from_hex(AUTHOR).unwrap()), + None, + ) + .unwrap() + } + + fn signing_runtime() -> RadrootsRuntime { + let signer = radroots_nostr::signing::LocalSigner::new( + radroots_nostr::key::SecretKey::parse(SECRET).unwrap(), + ) + .unwrap(); + RadrootsRuntime::from_client_builder( + ClientBuilder::memory_default(), + Some(PublicKey::from_hex(AUTHOR).unwrap()), + Some(std::sync::Arc::new(signer)), ) .unwrap() } @@ -1184,8 +1285,8 @@ mod tests { } #[tokio::test] - async fn all_five_add_flows_queue_without_network_access() { - let runtime = runtime(); + async fn all_five_add_flows_queue_and_sign_without_network_access() { + let runtime = signing_runtime(); let (photo, media) = photo_command(); let commands = [ ( @@ -1238,10 +1339,69 @@ mod tests { .unwrap(); assert_eq!(queued.state(), Phase1OutboxState::Queued); assert_eq!(queued.command_type(), CANONICAL_ADD_COMMAND_TYPES[index]); + let signed = runtime + .phase1_sign_queued_draft(id, queued.draft().revision().get()) + .await + .unwrap(); + assert_eq!(signed.state(), Phase1OutboxState::Signed); + assert_eq!( + signed + .push() + .and_then(|push| push.artifact().signed()) + .expect("signed artifact") + .event() + .kind(), + match index { + 0..=2 => 1, + 3 => 31_922, + 4 => 30_402, + _ => unreachable!(), + } + ); } } #[tokio::test] + async fn blossom_authorization_uses_the_same_opaque_signer_but_never_the_outbox() { + use radroots_blossom::authorization::{ + AuthorizationContent, AuthorizationTarget, AuthorizationValidation, ServerDomain, + }; + + let runtime = signing_runtime(); + let hash = BlossomSha256::digest(b"exact upload bytes"); + let server = ServerDomain::parse("media.example").unwrap(); + let claim = AuthoredUploadClaim::new( + AuthorizationContent::parse("Upload exact Radroots image").unwrap(), + server.clone(), + hash, + 1_900_000_000, + 60, + ) + .unwrap(); + let header = runtime + .phase1_authorize_blossom_upload( + [71; 16], + [72; 16], + claim, + u64::MAX, + Phase1CancellationPolicy::LocalCooperative, + ) + .await + .unwrap(); + let verified = radroots_nostr::blossom::decode_verify_authorization_header( + header.as_str(), + &AuthorizationValidation::bud11( + AuthorizationTarget::Upload(hash), + server, + 1_900_000_001, + ), + ) + .unwrap(); + assert_eq!(verified.claim().hashes(), &[hash]); + assert!(runtime.phase1_draft_heads(10).await.unwrap().is_empty()); + } + + #[tokio::test] async fn cancellation_is_terminal_and_preserves_operation_evidence() { let runtime = runtime(); let id = [8; 16]; diff --git a/crates/mobile_core/tests/durable_runtime.rs b/crates/mobile_core/tests/durable_runtime.rs @@ -109,21 +109,3 @@ async fn unrecognized_sqlite_bytes_are_corruption_classified() { assert_eq!(report.code, "storage_integrity_failed"); assert!(!report.retryable); } - -#[cfg(feature = "mobile-social")] -#[tokio::test] -async fn signer_selection_cannot_cross_the_authenticated_store_identity() { - const OTHER_SECRET: &str = "0000000000000000000000000000000000000000000000000000000000000002"; - - let root = tempfile::tempdir().expect("tempdir"); - let runtime = RuntimeBuilder::new(support::store(root.path())) - .build() - .await - .expect("runtime"); - let error = runtime - .nostr_identity_restore_host_custody_secret(OTHER_SECRET.to_owned(), None, true) - .expect_err("different identity must not select this user store"); - assert!(matches!(error, RadrootsAppError::Runtime(_))); - assert!(!runtime.nostr_identity_has_selected_signing_identity()); - runtime.shutdown().await.expect("shutdown"); -} diff --git a/crates/mobile_core/tests/package_boundary.rs b/crates/mobile_core/tests/package_boundary.rs @@ -4,13 +4,13 @@ const ERROR: &str = include_str!("../src/error.rs"); const RUNTIME: &str = include_str!("../src/runtime/mod.rs"); const APP_INFO: &str = include_str!("../src/runtime/app_info.rs"); const INFO: &str = include_str!("../src/runtime/info.rs"); -const KEY_MANAGEMENT: &str = include_str!("../src/runtime/key_management.rs"); -const NOSTR: &str = include_str!("../src/runtime/nostr.rs"); const PRODUCT_SURFACE: &str = include_str!("../src/runtime/product_surface.rs"); +const PRODUCT_AUTHORING: &str = include_str!("../src/runtime/product_surface/authoring.rs"); const PRODUCT_CONTEXT: &str = include_str!("../src/runtime/product_surface/context.rs"); const PRODUCT_CURSOR: &str = include_str!("../src/runtime/product_surface/cursor.rs"); const PRODUCT_IDENTITY: &str = include_str!("../src/runtime/product_surface/identity.rs"); const PRODUCT_MODEL: &str = include_str!("../src/runtime/product_surface/model.rs"); +const PRODUCT_OUTBOX: &str = include_str!("../src/runtime/product_surface/outbox.rs"); const PRODUCT_PROJECTION: &str = include_str!("../src/runtime/product_surface/projection.rs"); const PRODUCT_RANKING: &str = include_str!("../src/runtime/product_surface/ranking.rs"); const SDK: &str = include_str!("../src/runtime/sdk.rs"); @@ -26,13 +26,16 @@ fn core_owns_no_uniffi_or_process_global_logging_policy() { ("src/runtime/mod.rs", RUNTIME), ("src/runtime/app_info.rs", APP_INFO), ("src/runtime/info.rs", INFO), - ("src/runtime/key_management.rs", KEY_MANAGEMENT), - ("src/runtime/nostr.rs", NOSTR), ("src/runtime/product_surface.rs", PRODUCT_SURFACE), + ( + "src/runtime/product_surface/authoring.rs", + PRODUCT_AUTHORING, + ), ("src/runtime/product_surface/context.rs", PRODUCT_CONTEXT), ("src/runtime/product_surface/cursor.rs", PRODUCT_CURSOR), ("src/runtime/product_surface/identity.rs", PRODUCT_IDENTITY), ("src/runtime/product_surface/model.rs", PRODUCT_MODEL), + ("src/runtime/product_surface/outbox.rs", PRODUCT_OUTBOX), ( "src/runtime/product_surface/projection.rs", PRODUCT_PROJECTION, @@ -52,6 +55,22 @@ fn core_owns_no_uniffi_or_process_global_logging_policy() { } #[test] +fn mobile_runtime_has_no_secret_taking_or_local_signer_slot_surface() { + for source in [ + MANIFEST, + RUNTIME, + BUILDER, + PRODUCT_AUTHORING, + PRODUCT_OUTBOX, + ] { + assert!(!source.contains("signing::Slot")); + assert!(!source.contains("secret_key: String")); + assert!(!source.contains("Provider::slot")); + } + assert!(!MANIFEST.contains("radroots_sdk/local-signing")); +} + +#[test] fn production_runtime_requires_validated_sqlite_and_memory_is_test_only() { assert!(MANIFEST.contains("radroots_sdk = { workspace = true, features = [\"sqlite\"] }")); assert_eq!(BUILDER.matches("ClientBuilder::sqlite").count(), 1); diff --git a/crates/mobile_ffi/src/remote.rs b/crates/mobile_ffi/src/remote.rs @@ -2,8 +2,6 @@ use radroots_mobile_core::runtime::app_info::*; use radroots_mobile_core::runtime::info::*; -use radroots_mobile_core::runtime::key_management::*; -use radroots_mobile_core::runtime::nostr::*; use radroots_mobile_core::runtime::product_surface::*; use radroots_mobile_core::runtime::sdk::*; use radroots_mobile_core::{SdkErrorRecord, StoreErrorRecord}; @@ -89,81 +87,6 @@ pub struct SdkShutdownRecord { pub already_closed: bool, } -#[uniffi::remote(Record)] -pub struct NostrIdentityRecord { - pub id: String, - pub public_key_hex: String, - pub public_key_npub: String, - pub label: Option<String>, - pub is_selected: bool, -} - -#[uniffi::remote(Record)] -pub struct NostrIdentitySnapshot { - pub has_selected_signing_identity: bool, - pub selected_identity_id: Option<String>, - pub selected_npub: Option<String>, - pub identities: Vec<NostrIdentityRecord>, -} - -#[uniffi::remote(Record)] -pub struct NostrHostCustodyIdentity { - pub id: String, - pub public_key_hex: String, - pub public_key_npub: String, -} - -#[uniffi::remote(Enum)] -pub enum NostrLight { - Red, - Yellow, - Green, -} - -#[uniffi::remote(Record)] -pub struct NostrConnectionStatus { - pub light: NostrLight, - pub configured: bool, - pub source_available: bool, - pub sink_available: bool, - pub last_error: Option<String>, -} - -#[uniffi::remote(Record)] -pub struct NostrProfile { - pub name: Option<String>, - pub display_name: Option<String>, - pub nip05: Option<String>, - pub about: Option<String>, - pub website: Option<String>, - pub picture: Option<String>, - pub banner: Option<String>, - pub lud06: Option<String>, - pub lud16: Option<String>, - pub bot: Option<String>, -} - -#[uniffi::remote(Record)] -pub struct NostrProfileEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub profile: NostrProfile, -} - -#[uniffi::remote(Record)] -pub struct NostrPost { - pub content: String, -} - -#[uniffi::remote(Record)] -pub struct NostrPostEventMetadata { - pub id: String, - pub author: String, - pub published_at: u64, - pub post: NostrPost, -} - #[uniffi::remote(Enum)] pub enum TodayCardType { Update, diff --git a/crates/mobile_ffi/src/runtime.rs b/crates/mobile_ffi/src/runtime.rs @@ -1,7 +1,5 @@ use radroots_mobile_core::runtime::{ info::RuntimeInfo, - key_management::{NostrHostCustodyIdentity, NostrIdentityRecord, NostrIdentitySnapshot}, - nostr::{NostrConnectionStatus, NostrPostEventMetadata, NostrProfileEventMetadata}, product_surface::{AddCommandType, CardAddParity, LocalNetwork, TodayCardType}, sdk::{SdkCapabilityRecord, SdkShutdownRecord, SdkStorageStatusRecord}, }; @@ -91,147 +89,6 @@ impl RadrootsRuntime { self.inner.sdk_storage_status().await.map_err(Into::into) } - pub fn nostr_identity_has_selected_signing_identity(&self) -> bool { - self.inner.nostr_identity_has_selected_signing_identity() - } - - pub fn nostr_identity_selected_npub(&self) -> Option<String> { - self.inner.nostr_identity_selected_npub() - } - - pub fn nostr_identity_list(&self) -> Result<Vec<NostrIdentityRecord>, RadrootsAppError> { - self.inner.nostr_identity_list().map_err(Into::into) - } - - pub fn nostr_identity_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> { - self.inner.nostr_identity_list_ids().map_err(Into::into) - } - - pub fn nostr_identity_snapshot(&self) -> Result<NostrIdentitySnapshot, RadrootsAppError> { - self.inner.nostr_identity_snapshot().map_err(Into::into) - } - - pub fn nostr_identity_validate_host_custody_secret( - &self, - secret_key: String, - ) -> Result<NostrHostCustodyIdentity, RadrootsAppError> { - self.inner - .nostr_identity_validate_host_custody_secret(secret_key) - .map_err(Into::into) - } - - pub fn nostr_identity_restore_host_custody_secret( - &self, - secret_key: String, - label: Option<String>, - make_selected: bool, - ) -> Result<NostrIdentityRecord, RadrootsAppError> { - self.inner - .nostr_identity_restore_host_custody_secret(secret_key, label, make_selected) - .map_err(Into::into) - } - - pub fn nostr_identity_select(&self, identity_id: String) -> Result<(), RadrootsAppError> { - self.inner - .nostr_identity_select(identity_id) - .map_err(Into::into) - } - - pub fn nostr_identity_remove(&self, identity_id: String) -> Result<(), RadrootsAppError> { - self.inner - .nostr_identity_remove(identity_id) - .map_err(Into::into) - } - - pub fn nostr_identity_lock_host_custody_runtime(&self) -> Result<(), RadrootsAppError> { - self.inner - .nostr_identity_lock_host_custody_runtime() - .map_err(Into::into) - } - - pub fn nostr_identity_reset_host_custody_runtime(&self) -> Result<(), RadrootsAppError> { - self.inner - .nostr_identity_reset_host_custody_runtime() - .map_err(Into::into) - } - - pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> { - self.inner - .nostr_set_default_relays(relays) - .map_err(Into::into) - } - - pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> { - self.inner - .nostr_connect_if_key_present() - .map_err(Into::into) - } - - pub async fn nostr_connection_status(&self) -> Result<NostrConnectionStatus, RadrootsAppError> { - self.inner - .nostr_connection_status() - .await - .map_err(Into::into) - } - - pub async fn nostr_profile_for_self( - &self, - ) -> Result<Option<NostrProfileEventMetadata>, RadrootsAppError> { - self.inner - .nostr_profile_for_self() - .await - .map_err(Into::into) - } - - pub async fn nostr_post_profile( - &self, - name: Option<String>, - display_name: Option<String>, - nip05: Option<String>, - about: Option<String>, - ) -> Result<String, RadrootsAppError> { - self.inner - .nostr_post_profile(name, display_name, nip05, about) - .await - .map_err(Into::into) - } - - pub async fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> { - self.inner - .nostr_post_text_note(content) - .await - .map_err(Into::into) - } - - pub async fn nostr_fetch_text_notes( - &self, - limit: u16, - since_unix: Option<u64>, - ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> { - self.inner - .nostr_fetch_text_notes(limit, since_unix) - .await - .map_err(Into::into) - } - - pub async fn nostr_post_reply( - &self, - parent_event_id_hex: String, - parent_author_hex: String, - content: String, - root_event_id_hex: Option<String>, - ) -> Result<String, RadrootsAppError> { - self.inner - .nostr_post_reply( - parent_event_id_hex, - parent_author_hex, - content, - root_event_id_hex, - ) - .await - .map_err(Into::into) - } - pub fn phase1_card_types(&self) -> Vec<TodayCardType> { self.inner.phase1_card_types() } diff --git a/crates/mobile_ffi/tests/runtime_delegation.rs b/crates/mobile_ffi/tests/runtime_delegation.rs @@ -3,8 +3,6 @@ use radroots_mobile_ffi::RadrootsAppError; mod support; -const SECRET: &str = "0000000000000000000000000000000000000000000000000000000000000001"; - #[tokio::test] async fn native_boundary_delegates_the_complete_core_surface() { let (_root, runtime) = support::runtime().await; @@ -27,99 +25,6 @@ async fn native_boundary_delegates_the_complete_core_surface() { "sqlite" ); - assert!(!runtime.nostr_identity_has_selected_signing_identity()); - assert!(runtime.nostr_identity_selected_npub().is_none()); - assert!( - runtime - .nostr_identity_list() - .expect("empty list") - .is_empty() - ); - assert!( - runtime - .nostr_identity_list_ids() - .expect("empty identifiers") - .is_empty() - ); - assert!( - runtime - .nostr_identity_snapshot() - .expect("empty snapshot") - .identities - .is_empty() - ); - let validated = runtime - .nostr_identity_validate_host_custody_secret(SECRET.to_owned()) - .expect("valid secret"); - let staged = runtime - .nostr_identity_restore_host_custody_secret( - SECRET.to_owned(), - Some("staged".to_owned()), - false, - ) - .expect("staged identity"); - assert_eq!(validated.id, staged.id); - let selected = runtime - .nostr_identity_restore_host_custody_secret( - SECRET.to_owned(), - Some("selected".to_owned()), - true, - ) - .expect("selected identity"); - assert_eq!( - runtime.nostr_identity_selected_npub(), - Some(selected.public_key_npub.clone()) - ); - runtime - .nostr_identity_select(selected.id.clone()) - .expect("select installed"); - assert!(runtime.nostr_identity_select("missing".to_owned()).is_err()); - runtime - .nostr_identity_remove("missing".to_owned()) - .expect("removing missing identity is idempotent"); - runtime - .nostr_identity_lock_host_custody_runtime() - .expect("lock identity"); - runtime - .nostr_identity_reset_host_custody_runtime() - .expect("reset identity"); - - assert!(runtime.nostr_set_default_relays(Vec::new()).is_err()); - assert!(runtime.nostr_connect_if_key_present().is_err()); - assert!( - runtime - .nostr_connection_status() - .await - .expect("unconfigured status") - .last_error - .is_none() - ); - assert!(runtime.nostr_profile_for_self().await.is_err()); - assert!( - runtime - .nostr_post_profile(None, None, None, None) - .await - .is_err() - ); - assert!( - runtime - .nostr_post_text_note("post".to_owned()) - .await - .is_err() - ); - assert!(runtime.nostr_fetch_text_notes(1, None).await.is_err()); - assert!(matches!( - runtime - .nostr_post_reply( - "parent".to_owned(), - "author".to_owned(), - "reply".to_owned(), - Some("different-root".to_owned()), - ) - .await, - Err(RadrootsAppError::Unsupported(_)) - )); - assert_eq!( runtime.phase1_card_types(), vec![ diff --git a/crates/nostr/README.md b/crates/nostr/README.md @@ -100,10 +100,11 @@ persistence commit point: the only successful result is the value returned to the caller. A transport or host that later publishes or stores that value owns its own cancellation and commit semantics. -The `blossom` module creates and verifies signed `Authorization: Nostr` values -but never sends an HTTP request. The `nip17` module creates and opens gift-wrap -events. It does not select relays, deliver events, retry operations, or persist -message state. +The `blossom` module converts exact, verified signer output into and from +signed `Authorization: Nostr` values but never sends an HTTP request. BUD-11 +plans remain distinct from relay-authored plans. The `nip17` module creates and +opens gift-wrap events. It does not select relays, deliver events, retry +operations, or persist message state. ## Serialization contract diff --git a/crates/nostr/src/blossom.rs b/crates/nostr/src/blossom.rs @@ -49,6 +49,28 @@ impl fmt::Debug for SignedAuthorization { } impl SignedAuthorization { + /// Converts an already verified exact signer result into an HTTP-only + /// authorization value, rejecting every non-BUD-11 event. + pub fn from_signed_event( + signed_event: &radroots_event::SignedEvent, + ) -> Result<Self, AuthorizationError> { + if signed_event.kind() != u32::from(RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND) { + return Err(AuthorizationError::InvalidEventKind { + actual: u64::from(signed_event.kind()), + }); + } + let event: RadrootsNostrEvent = serde_json::from_str(signed_event.raw_json()) + .map_err(|_| AuthorizationError::InvalidEventJson)?; + // `SignedEvent` has already verified that its retained wire ID matches + // the canonical event preimage. Repeating that invariant here would + // create an unreachable failure branch; the signature remains an + // intentionally separate verification stage on that type. + if !event.verify_signature() { + return Err(AuthorizationError::InvalidEventSignature); + } + Ok(Self { event }) + } + pub fn event_id(&self) -> RadrootsNostrEventId { self.event.id } @@ -269,6 +291,15 @@ pub fn encode_authorization_header(authorization: &SignedAuthorization) -> Autho AuthorizationHeader(format!("{AUTHORIZATION_SCHEME}{payload}")) } +/// Converts one verified generic signer receipt into a canonical BUD-11 HTTP +/// authorization value without exposing or accepting secret key material. +pub fn encode_signed_event_authorization_header( + signed_event: &radroots_event::SignedEvent, +) -> Result<AuthorizationHeader, AuthorizationError> { + SignedAuthorization::from_signed_event(signed_event) + .map(|authorization| encode_authorization_header(&authorization)) +} + /// Decode, authenticate, parse, and validate a BUD-11 authorization value. pub fn decode_verify_authorization_header( header: &str, diff --git a/crates/nostr/src/plan_signing.rs b/crates/nostr/src/plan_signing.rs @@ -10,6 +10,8 @@ use crate::{ }, }; use radroots_event_codec::authoring::AuthoredEventPlan; +#[cfg(feature = "signing")] +use radroots_signing::SignRequest; #[cfg(feature = "signing")] use nostr::JsonUtil; @@ -51,12 +53,43 @@ pub(crate) fn unsigned_event_from_plan( } #[cfg(feature = "signing")] -pub(crate) fn sign_authored_plan( +fn unsigned_event_from_request(request: &SignRequest) -> Result<nostr::UnsignedEvent, Error> { + let kind = u16::try_from(request.kind()).map_err(|_| Error::KindOutOfRange { + kind: request.kind(), + max: u16::MAX, + })?; + let tags = request + .tags() + .iter() + .cloned() + .map(RadrootsNostrTag::parse) + .collect::<Result<alloc::vec::Vec<_>, _>>() + .map_err(|_| Error::TagConversion)?; + let expected_event_id = + RadrootsNostrEventId::from_slice(request.expected_event_id().as_bytes()).map_err(|_| { + Error::EventConversion { + field: "expected_event_id", + } + })?; + let expected_public_key = + RadrootsNostrPublicKey::from_slice(request.expected_author().as_bytes()) + .map_err(|_| Error::EventConversion { field: "author" })?; + Ok(nostr::UnsignedEvent { + id: Some(expected_event_id), + pubkey: expected_public_key, + created_at: RadrootsNostrTimestamp::from_secs(request.created_at()), + kind: RadrootsNostrKind::Custom(kind), + tags: nostr::Tags::from_list(tags), + content: request.content().into(), + }) +} + +#[cfg(feature = "signing")] +pub(crate) fn sign_request( keys: &nostr::Keys, - plan: &AuthoredEventPlan, + request: &SignRequest, ) -> Result<SignedEvent, Error> { - let event = unsigned_event_from_plan(plan)?.sign_with_keys(keys)?; - validate_signed_event_matches_plan(&event, plan)?; + let event = unsigned_event_from_request(request)?.sign_with_keys(keys)?; let raw_json = event.as_json(); let wire = Nip01EventWire::parse_json(&raw_json)?; SignedEvent::from_wire_verified_id(wire, raw_json).map_err(Into::into) diff --git a/crates/nostr/src/signing.rs b/crates/nostr/src/signing.rs @@ -88,10 +88,10 @@ impl Signer for LocalSigner { &SignProgress::stage(SignProgressStage::Validating) .expect("validating has no challenge"), ); - if request.plan().author() != &self.public_key { + if request.expected_author() != &self.public_key { return Err(SigningError::new(Kind::AuthorizationDenied)); } - let signed_event = crate::plan_signing::sign_authored_plan(&self.keys, request.plan()) + let signed_event = crate::plan_signing::sign_request(&self.keys, &request) .map_err(normalize_nostr_error)?; request.report_progress( &SignProgress::stage(SignProgressStage::VerifyingOutput) @@ -205,7 +205,7 @@ mod tests { ReplayCapability::LocalReplaySafe ); let request = request(); - let expected_id = request.plan().expected_event_id().to_hex(); + let expected_id = request.expected_event_id().to_hex(); let receipt = signer.sign(request).await.unwrap(); assert_eq!(receipt.signed_event().id_str(), expected_id); assert_eq!(receipt.completed_at_unix_ms(), DEADLINE_MS - 1); diff --git a/crates/nostr/tests/blossom_conformance.rs b/crates/nostr/tests/blossom_conformance.rs @@ -11,9 +11,10 @@ use radroots_blossom::{ ServerDomain, ServerScopeRequirement, }, }; +use radroots_event::{SignedEvent, wire::Nip01EventWire}; use radroots_nostr::blossom::{ - AuthorizationError, decode_verify_authorization_header, encode_authorization_header, - sign_authorization, + AuthorizationError, SignedAuthorization, decode_verify_authorization_header, + encode_authorization_header, encode_signed_event_authorization_header, sign_authorization, }; use radroots_nostr::{ event::{ @@ -359,6 +360,12 @@ fn sign_raw(kind: u16, content: &str, tags: Vec<RadrootsNostrTag>) -> RadrootsNo .expect("raw test event signs") } +fn exact_signed_event(event: &RadrootsNostrEvent) -> SignedEvent { + let raw_json = serde_json::to_string(event).expect("test event JSON"); + let wire = Nip01EventWire::parse_json(&raw_json).expect("test event wire"); + SignedEvent::from_wire_verified_id(wire, raw_json).expect("test event has a verified ID") +} + #[test] fn blossom_authored_claim_signs_and_roundtrips_without_signature_assumptions() { let claim = authored_claim(); @@ -406,6 +413,44 @@ fn blossom_authored_claim_signs_and_roundtrips_without_signature_assumptions() { } #[test] +fn opaque_signed_event_conversion_rechecks_kind_and_signature() { + let signed = sign_authorization(&keys(), &authored_claim()).expect("sign authorization"); + let expected_header = encode_authorization_header(&signed); + let event = event_from_header(expected_header.as_str()); + let exact_event = exact_signed_event(&event); + + let converted = + SignedAuthorization::from_signed_event(&exact_event).expect("convert exact BUD-11 event"); + assert_eq!(converted.event_id(), event.id); + assert_eq!(converted.author(), event.pubkey); + assert_eq!(converted.created_at(), event.created_at); + assert_eq!( + encode_signed_event_authorization_header(&exact_event).unwrap(), + expected_header + ); + + let wrong_kind = exact_signed_event(&sign_raw(1, "", Vec::new())); + assert_eq!( + SignedAuthorization::from_signed_event(&wrong_kind), + Err(AuthorizationError::InvalidEventKind { actual: 1 }) + ); + + let mut invalid_signature = event; + invalid_signature.sig = sign_raw( + RADROOTS_BLOSSOM_AUTHORIZATION_EVENT_KIND, + "different", + Vec::new(), + ) + .sig; + assert!(invalid_signature.verify_id()); + assert!(!invalid_signature.verify_signature()); + assert_eq!( + SignedAuthorization::from_signed_event(&exact_signed_event(&invalid_signature)), + Err(AuthorizationError::InvalidEventSignature) + ); +} + +#[test] fn blossom_header_rejects_noncanonical_and_malformed_encodings() { let signed = sign_authorization(&keys(), &authored_claim()).expect("sign authored authorization"); diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs @@ -580,6 +580,15 @@ fn superseded_surface_retirement_is_explicit_and_release_bounded() { "reviewed API baseline retains forbidden surface `{forbidden}`" ); } + for required in [ + "pub fn radroots_nostr::blossom::SignedAuthorization::from_signed_event", + "pub fn radroots_nostr::blossom::encode_signed_event_authorization_header", + ] { + assert!( + PUBLIC_API.contains(required), + "reviewed API baseline is missing `{required}`" + ); + } } fn rust_sources(root: &Path) -> Vec<PathBuf> { diff --git a/crates/radroots/Cargo.toml b/crates/radroots/Cargo.toml @@ -26,7 +26,7 @@ publish = ["crates-io"] autoexamples = false [package.metadata.docs.rs] -features = ["client", "nostr", "nip46", "geonames", "knowledge"] +features = ["client", "blossom", "nostr", "nip46", "geonames", "knowledge"] [lints] workspace = true @@ -48,6 +48,7 @@ default = ["client"] client = ["radroots_sdk/default"] native = ["client", "radroots_sdk/native"] nostr = ["client", "radroots_sdk/nostr"] +blossom = ["client", "radroots_sdk/blossom"] nip46 = ["nostr", "radroots_sdk/nip46"] radrootsd = ["client", "radroots_sdk/radrootsd"] geonames = ["client", "radroots_sdk/geonames"] diff --git a/crates/radroots/README.md b/crates/radroots/README.md @@ -62,6 +62,7 @@ are Rust paths, not a second persistence format. | --- | --- | | `client` | safe memory-backed client; the default | | `native` | explicit SQLite, sync, and local-signing SDK capabilities | +| `blossom` | HTTP-only BUD-11 upload authorization through an opaque host signer | | `nostr` | explicit Nostr source/sink composition | | `nip46` | host-owned NIP-46 signer composition; implies `nostr` | | `radrootsd` | explicitly invoked daemon delivery | @@ -91,4 +92,4 @@ use radroots::sdk; The normative package charter is the [`radroots` release-v1 specification](../../docs/specs/radroots_crates_release_v1.md#19-radroots). The reviewed pre-release public API is recorded in the -[`radroots` baseline](../../docs/api/radroots-0.1.0-alpha.txt). +[`radroots` baseline](../../docs/api/radroots.txt). diff --git a/crates/radroots/src/signing.rs b/crates/radroots/src/signing.rs @@ -1,3 +1,8 @@ //! Curated signing entry points. -pub use radroots_sdk::signing::{Mode, Provider}; +pub use radroots_sdk::signing::{ + BlossomAuthorizationPlan, Mode, Operations, Provider, blossom_upload_request, +}; + +#[cfg(any(feature = "blossom", feature = "nostr", feature = "full"))] +pub use radroots_sdk::signing::{AuthorizationHeader, BlossomSigningError}; diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -30,6 +30,7 @@ autoexamples = false features = [ "memory", "sync", + "blossom", "nostr", "nip46", "local-signing", @@ -45,7 +46,8 @@ default = ["memory"] memory = ["radroots_storage/memory"] sqlite = ["dep:radroots_storage_sqlite"] sync = ["dep:radroots_sync", "dep:uuid"] -nostr = ["sync", "dep:radroots_nostr", "dep:radroots_transport_nostr"] +nostr = ["sync", "blossom", "dep:radroots_transport_nostr"] +blossom = ["dep:radroots_nostr", "radroots_nostr/blossom"] nip46 = ["nostr", "dep:radroots_nostr_connect"] local-signing = [ "dep:radroots_nostr", @@ -95,6 +97,7 @@ uuid = { workspace = true, optional = true, features = ["v4"] } [dev-dependencies] nostr = { workspace = true, features = ["std"] } +radroots_blossom = { workspace = true } serde_json = { workspace = true, features = ["std"] } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/sdk/README.md b/crates/sdk/README.md @@ -68,19 +68,20 @@ are activated only by their owning feature. The supported qualification matrix is: ```sh -cargo check -p radroots_sdk --all-targets --no-default-features +cargo check -p radroots_sdk --lib --no-default-features cargo check -p radroots_sdk --all-targets -cargo check -p radroots_sdk --all-targets --no-default-features --features memory -cargo check -p radroots_sdk --all-targets --no-default-features --features sqlite -cargo check -p radroots_sdk --all-targets --no-default-features --features sync -cargo check -p radroots_sdk --all-targets --no-default-features --features nostr -cargo check -p radroots_sdk --all-targets --no-default-features --features nip46 -cargo check -p radroots_sdk --all-targets --no-default-features --features local-signing -cargo check -p radroots_sdk --all-targets --no-default-features --features radrootsd -cargo check -p radroots_sdk --all-targets --no-default-features --features geonames -cargo check -p radroots_sdk --all-targets --no-default-features --features knowledge -cargo check -p radroots_sdk --all-targets --no-default-features --features native -cargo check -p radroots_sdk --all-targets --no-default-features --features full +cargo check -p radroots_sdk --lib --no-default-features --features memory +cargo check -p radroots_sdk --lib --no-default-features --features sqlite +cargo check -p radroots_sdk --lib --no-default-features --features sync +cargo check -p radroots_sdk --lib --no-default-features --features blossom +cargo check -p radroots_sdk --lib --no-default-features --features nostr +cargo check -p radroots_sdk --lib --no-default-features --features nip46 +cargo check -p radroots_sdk --lib --no-default-features --features local-signing +cargo check -p radroots_sdk --lib --no-default-features --features radrootsd +cargo check -p radroots_sdk --lib --no-default-features --features geonames +cargo check -p radroots_sdk --lib --no-default-features --features knowledge +cargo check -p radroots_sdk --lib --no-default-features --features native +cargo check -p radroots_sdk --lib --no-default-features --features full cargo check -p radroots_sdk --all-targets --all-features ``` @@ -92,20 +93,22 @@ feature or constructing an empty builder creates no resource. Signers, event sources, event sinks, and the sync engine are injected separately. Native mobile hosts can retain one client while changing host-owned identity -and relay selection. `signing::Slot` accepts a key restored from secure host -storage or generates a one-time `nsec` handoff; it never persists that secret. -`transport::NostrSlot` validates a complete relay set before atomically -installing it. `ClientBuilder::host_sync(sync::HostPolicy)` explicitly opts -SDK-created memory or SQLite storage into system-clock and random operation-ID -policy without creating a runtime, timer, retry loop, or worker. - -With `sync`, `nostr`, and `local-signing`, `Client::social()` exposes bounded -profile/post fetch and publish operations. Fetch verifies and durably ingests -each accepted event. Publish durably commits a signed event, then performs one -explicit delivery pass and reports whether delivery remains pending. Host UI -lifecycle code owns polling, background policy, keychain access, and any later -retry. Profile authoring is deliberately media-free until the host can supply -the canonical byte-verified media descriptor required by the event contract. +and relay selection. The host injects one opaque implementation of the +canonical `radroots_signing::Signer` SPI; the SDK has no mutable secret slot and +accepts no secret string. `transport::NostrSlot` validates a complete relay set +before atomically installing it. `ClientBuilder::host_sync(sync::HostPolicy)` +explicitly opts SDK-created memory or SQLite storage into system-clock and +random operation-ID policy without creating a runtime, timer, retry loop, or +worker. + +`Client::signing()` exposes focused operations over that opaque signer. Every +returned event is independently rebound to the exact request, expected public +key, caller-observed deadline, cancellation signal, event ID, fields, and +signature. The `blossom` feature adds a domain-separated BUD-11 upload plan and +canonical HTTP authorization header; this credential type cannot enter the +relay push API and is never persisted by the SDK. Durable authored relay work +continues through the canonical sync operations. Host UI lifecycle code owns +polling, background policy, native key custody, and explicit retry. Transport profiles are explicit. `Profile::local_only()` contains no target. `Profile::delivery(...)` retains the exact canonical target set and @@ -146,12 +149,12 @@ errors and daemon failures use stable, redacted classifications while retaining private source chains for local diagnostics. Signer material and bearer credentials are caller-owned capabilities. The SDK -does not read a keyring, persist secrets, or include credentials -in `Debug`, `Display`, diagnostics, receipts, or public error text. Hosts remain -responsible for protecting source chains and any lower-level logs they choose -to expose. When explicitly requested, `signing::Slot::generate` creates one -ephemeral key and immediately hands its only persistence representation to the -host for secure custody. +does not read a keyring, accept or generate secret strings, persist secrets, or +include credentials in `Debug`, `Display`, diagnostics, receipts, or public +error text. A concrete local adapter may be composed explicitly outside the +mobile surface, but its key remains opaque. Hosts remain responsible for +native custody and for protecting source chains and lower-level logs they +choose to expose. ## Daemon execution @@ -168,4 +171,4 @@ publication remains blocked pending the approval packet and a separately authorized operator step. The crate is licensed under `MIT OR Apache-2.0`. The reviewed all-features public API baseline is recorded at -[`docs/api/radroots_sdk-0.1.0-alpha.txt`](../../docs/api/radroots_sdk-0.1.0-alpha.txt). +[`docs/api/radroots_sdk.txt`](../../docs/api/radroots_sdk.txt). diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -10,8 +10,6 @@ use std::{ use radroots_signing::Signer; use radroots_storage::Storage; -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -use radroots_storage::authored_delivery::AuthoredDeliveryState; #[cfg(feature = "memory")] use radroots_storage::{event::SourceGeneration, memory::MemoryStorage}; use radroots_transport::{EventSink, EventSource}; @@ -40,10 +38,6 @@ pub struct ClientBuilder { sync: Option<radroots_sync::Engine>, #[cfg(feature = "sync")] host_sync: Option<crate::sync::HostPolicy>, - #[cfg(feature = "local-signing")] - signing_slot: Option<crate::signing::Slot>, - #[cfg(feature = "nostr")] - nostr_slot: Option<crate::transport::NostrSlot>, capability_availability: BTreeMap<CapabilityId, Availability>, explicitly_configured_capabilities: BTreeSet<CapabilityId>, } @@ -55,10 +49,6 @@ struct ClientInner { sink: Option<Arc<dyn EventSink>>, #[cfg(feature = "sync")] sync: Option<radroots_sync::Engine>, - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - signing_slot: Option<crate::signing::Slot>, - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - nostr_slot: Option<crate::transport::NostrSlot>, capability_availability: BTreeMap<CapabilityId, Availability>, explicitly_configured_capabilities: BTreeSet<CapabilityId>, lifecycle: AtomicU8, @@ -69,225 +59,6 @@ const CLOSING: u8 = 1; const CLOSE_RETRY_REQUIRED: u8 = 2; const CLOSED: u8 = 3; -/// Host-authored, media-free profile replacement. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ProfileDraft { - name: String, - display_name: Option<String>, - about: Option<String>, - nip05: Option<String>, - bot: Option<bool>, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl ProfileDraft { - /// Creates a complete replacement with the required canonical name. - #[must_use] - pub fn new(name: impl Into<String>) -> Self { - Self { - name: name.into(), - display_name: None, - about: None, - nip05: None, - bot: None, - } - } - - /// Sets the optional display name. - #[must_use] - pub fn with_display_name(mut self, value: impl Into<String>) -> Self { - self.display_name = Some(value.into()); - self - } - - /// Sets the optional profile description. - #[must_use] - pub fn with_about(mut self, value: impl Into<String>) -> Self { - self.about = Some(value.into()); - self - } - - /// Sets a syntax-checked NIP-05 identifier at publish time. - #[must_use] - pub fn with_nip05(mut self, value: impl Into<String>) -> Self { - self.nip05 = Some(value.into()); - self - } - - /// Sets the optional NIP-05 bot marker. - #[must_use] - pub const fn with_bot(mut self, value: bool) -> Self { - self.bot = Some(value); - self - } -} - -/// One verified, durably ingested profile observation. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ProfileEvent { - event_id: String, - author: String, - created_at: u64, - name: Option<String>, - display_name: Option<String>, - about: Option<String>, - picture: Option<String>, - banner: Option<String>, - nip05: Option<String>, - bot: Option<bool>, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl ProfileEvent { - /// Returns the canonical event identifier. - pub fn event_id(&self) -> &str { - self.event_id.as_str() - } - /// Returns the canonical author public key. - pub fn author(&self) -> &str { - self.author.as_str() - } - /// Returns the event timestamp in Unix seconds. - pub const fn created_at(&self) -> u64 { - self.created_at - } - /// Returns the projected profile name. - pub fn name(&self) -> Option<&str> { - self.name.as_deref() - } - /// Returns the projected display name. - pub fn display_name(&self) -> Option<&str> { - self.display_name.as_deref() - } - /// Returns the projected description. - pub fn about(&self) -> Option<&str> { - self.about.as_deref() - } - /// Returns the unverified inbound picture reference. - pub fn picture(&self) -> Option<&str> { - self.picture.as_deref() - } - /// Returns the unverified inbound banner reference. - pub fn banner(&self) -> Option<&str> { - self.banner.as_deref() - } - /// Returns the syntax-checked, unresolved NIP-05 identifier. - pub fn nip05(&self) -> Option<&str> { - self.nip05.as_deref() - } - /// Returns the optional bot marker. - pub const fn bot(&self) -> Option<bool> { - self.bot - } -} - -/// One verified, durably ingested kind-1 social event. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PostEvent { - event_id: String, - author: String, - created_at: u64, - content: String, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl PostEvent { - /// Returns the canonical event identifier. - pub fn event_id(&self) -> &str { - self.event_id.as_str() - } - /// Returns the canonical author public key. - pub fn author(&self) -> &str { - self.author.as_str() - } - /// Returns the event timestamp in Unix seconds. - pub const fn created_at(&self) -> u64 { - self.created_at - } - /// Returns the canonical event content. - pub fn content(&self) -> &str { - self.content.as_str() - } -} - -/// Result of one explicit local commit followed by one delivery pass. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublishReceipt { - event_id: String, - replay: bool, - delivery_state: AuthoredDeliveryState, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl PublishReceipt { - /// Returns the canonical signed event identifier committed locally. - pub fn event_id(&self) -> &str { - self.event_id.as_str() - } - /// Returns whether preparation replayed an identical durable operation. - pub const fn is_replay(&self) -> bool { - self.replay - } - /// Returns the complete durable delivery state after this explicit pass. - pub const fn delivery_state(&self) -> AuthoredDeliveryState { - self.delivery_state - } - /// Returns whether this explicit pass satisfied the delivery policy. - pub const fn is_delivered(&self) -> bool { - matches!(self.delivery_state, AuthoredDeliveryState::Satisfied) - } - /// Returns whether durable local intent remains eligible for delivery. - pub const fn is_delivery_pending(&self) -> bool { - matches!( - self.delivery_state, - AuthoredDeliveryState::Pending | AuthoredDeliveryState::Retryable - ) - } -} - -/// Passive status of the configured shared Nostr source and sink. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct TransportHealth { - configured: bool, - source_available: bool, - sink_available: bool, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl TransportHealth { - /// Returns whether a validated relay set is installed. - pub const fn is_configured(&self) -> bool { - self.configured - } - /// Returns whether passive source status is fully available. - pub const fn is_source_available(&self) -> bool { - self.source_available - } - /// Returns whether passive sink status is fully available. - pub const fn is_sink_available(&self) -> bool { - self.sink_available - } -} - -/// Borrowed high-level social operations over one shared SDK engine. -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -#[derive(Clone, Copy)] -pub struct SocialOperations<'a> { - client: &'a Client, -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -enum SocialDraft { - Profile(Box<radroots_event::profile::AuthoredProfile>), - Update(radroots_event::post::AuthoredUpdate), - Reply(radroots_event::post::reply::AuthoredNip10Reply), -} - impl ClientBuilder { /// Creates an empty builder with no hidden storage, network, signing, or /// runtime side effects. @@ -377,16 +148,7 @@ impl ClientBuilder { crate::signing::Mode::Nip46 => Some(CapabilityId::NIP46_SIGNING), crate::signing::Mode::Host => None, }; - #[cfg(feature = "local-signing")] - { - let (signer, slot) = provider.into_parts(); - self.signer = Some(signer); - self.signing_slot = slot; - } - #[cfg(not(feature = "local-signing"))] - { - self.signer = Some(provider.into_signer()); - } + self.signer = Some(provider.into_signer()); if let Some(capability) = capability { self.explicitly_configured_capabilities.insert(capability); self.capability_availability @@ -415,7 +177,6 @@ impl ClientBuilder { pub fn nostr(mut self, slot: crate::transport::NostrSlot) -> Self { self.source = Some(Arc::new(slot.clone())); self.sink = Some(Arc::new(slot.clone())); - self.nostr_slot = Some(slot); self } @@ -454,9 +215,6 @@ impl ClientBuilder { #[allow(unused_mut)] pub fn build(mut self) -> Result<Client> { let storage = self.storage.ok_or_else(Error::missing_storage)?; - if (self.signer.is_some(), self.sink.is_some()) == (true, false) { - return Err(Error::signer_without_sink()); - } #[cfg(feature = "sync")] if let Some(policy) = self.host_sync { let sync_storage = self @@ -484,10 +242,6 @@ impl ClientBuilder { sink: self.sink, #[cfg(feature = "sync")] sync: self.sync, - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - signing_slot: self.signing_slot, - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - nostr_slot: self.nostr_slot, capability_availability: self.capability_availability, explicitly_configured_capabilities: self.explicitly_configured_capabilities, lifecycle: AtomicU8::new(OPEN), @@ -551,6 +305,11 @@ impl Client { Ok(self.inner.signer.as_deref()) } + /// Returns focused high-level operations over the configured opaque signer. + pub fn signing(&self) -> Result<Option<crate::signing::Operations<'_>>> { + Ok(self.signer()?.map(crate::signing::Operations::new)) + } + /// Returns the injected inbound source, when pull is enabled. pub fn source(&self) -> Result<Option<&dyn EventSource>> { self.require_open()?; @@ -591,22 +350,6 @@ impl Client { .map(|sync| crate::trade::Operations::new(storage, sync))) } - /// Returns high-level shared social operations when the required explicit - /// signer, transport, and synchronization composition is present. - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - pub fn social(&self) -> Result<SocialOperations<'_>> { - self.require_open()?; - let social_composition = ( - self.inner.sync.is_some(), - self.inner.signing_slot.is_some(), - self.inner.nostr_slot.is_some(), - ); - if social_composition != (true, true, true) { - return Err(Error::shared_operation_unavailable()); - } - Ok(SocialOperations { client: self }) - } - /// Returns whether explicit close completed successfully or reached the /// lower storage commit point. #[must_use] @@ -666,296 +409,6 @@ impl Client { } } -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -impl SocialOperations<'_> { - /// Observes both transport directions without initiating relay work. - pub async fn transport_health(&self) -> Result<TransportHealth> { - use radroots_transport::capability::Availability as TransportAvailability; - let slot = self.nostr()?; - let configured = slot.targets().is_some(); - let source = radroots_transport::EventSource::status(slot) - .await - .map_err(|_| Error::shared_operation_failed_without_source())?; - let sink = radroots_transport::EventSink::status(slot) - .await - .map_err(|_| Error::shared_operation_failed_without_source())?; - Ok(TransportHealth { - configured, - source_available: source.availability() == TransportAvailability::Available, - sink_available: sink.availability() == TransportAvailability::Available, - }) - } - - /// Fetches, verifies, and durably ingests the latest profile for the active signer. - pub async fn fetch_profile_for_signer(&self) -> Result<Option<ProfileEvent>> { - let identity = self.identity()?; - let selector = radroots_transport::source::FetchSelector::all() - .with_kinds(vec![radroots_event::envelope::kind::KIND_PROFILE]) - .and_then(|selector| selector.with_authors(vec![identity.public_key()])) - .map_err(|_| Error::invalid_host_configuration_without_source())?; - let events = self.fetch(32, selector).await?; - let mut profiles = events - .into_iter() - .filter_map(|event| profile_event(&event).ok()) - .collect::<Vec<_>>(); - profiles.sort_by_key(|event| std::cmp::Reverse(event.created_at)); - Ok(profiles.into_iter().next()) - } - - /// Fetches, verifies, and durably ingests a bounded kind-1 page. - pub async fn fetch_posts( - &self, - limit: u16, - since_unix_seconds: Option<u64>, - ) -> Result<Vec<PostEvent>> { - let mut selector = radroots_transport::source::FetchSelector::all() - .with_kinds(vec![radroots_event::envelope::kind::KIND_POST]) - .map_err(|_| Error::invalid_host_configuration_without_source())?; - if let Some(since) = since_unix_seconds { - selector = selector - .with_since_unix_seconds(since) - .map_err(|_| Error::invalid_host_configuration_without_source())?; - } - let mut posts = self - .fetch(limit, selector) - .await? - .into_iter() - .map(|event| PostEvent { - event_id: event.id_hex(), - author: event.pubkey().to_hex(), - created_at: event.created_at(), - content: event.content().to_owned(), - }) - .collect::<Vec<_>>(); - posts.sort_by_key(|event| std::cmp::Reverse(event.created_at)); - Ok(posts) - } - - /// Publishes a complete media-free profile replacement. - pub async fn publish_profile(&self, draft: ProfileDraft) -> Result<PublishReceipt> { - let mut profile = radroots_event::profile::AuthoredProfile::new(draft.name) - .map_err(Error::invalid_host_configuration)?; - if let Some(value) = draft.display_name { - profile = profile.with_display_name(value); - } - if let Some(value) = draft.about { - profile = profile.with_about(value); - } - if let Some(value) = draft.nip05 { - profile = profile.with_nip05( - radroots_event::profile::Nip05Identifier::parse(value.as_str()) - .map_err(Error::invalid_host_configuration)?, - ); - } - if let Some(value) = draft.bot { - profile = profile.with_bot(value); - } - self.publish(SocialDraft::Profile(Box::new(profile))).await - } - - /// Publishes one strict root kind-1 update. - pub async fn publish_text(&self, content: impl Into<String>) -> Result<PublishReceipt> { - let update = radroots_event::post::AuthoredUpdate::new(content) - .map_err(Error::invalid_host_configuration)?; - self.publish(SocialDraft::Update(update)).await - } - - /// Publishes one strict direct NIP-10 reply. - pub async fn publish_reply( - &self, - content: impl Into<String>, - root_event_id: &str, - root_author: &str, - relay_hint: Option<&str>, - ) -> Result<PublishReceipt> { - let reference = radroots_event::post::reply::Nip10ReplyReference::parse( - root_event_id, - root_author, - relay_hint, - ) - .map_err(Error::invalid_host_configuration)?; - let reply = radroots_event::post::reply::AuthoredNip10Reply::direct(content, reference) - .map_err(Error::invalid_host_configuration)?; - self.publish(SocialDraft::Reply(reply)).await - } - - async fn fetch( - &self, - limit: u16, - selector: radroots_transport::source::FetchSelector, - ) -> Result<Vec<radroots_event::SignedEvent>> { - let slot = self.nostr()?; - let targets = slot - .targets() - .ok_or_else(Error::shared_operation_unavailable)?; - let request_id = format!("sdk-fetch-{}", uuid::Uuid::new_v4()); - let deadline = now_unix_ms()?.saturating_add(30_000); - let request = radroots_transport::FetchRequest::new( - request_id, - targets, - radroots_transport::source::FetchBounds::new(limit, deadline) - .map_err(|_| Error::invalid_host_configuration_without_source())?, - ) - .map_err(|_| Error::invalid_host_configuration_without_source())? - .with_selector(selector); - let page = radroots_transport::EventSource::fetch(slot, request) - .await - .map_err(|_| Error::shared_operation_failed_without_source())?; - let observed = page.events().to_vec(); - let receipt = self - .client - .sync()? - .ok_or_else(Error::shared_operation_unavailable)? - .ingest_batch( - observed.clone(), - &radroots_sync::ingest::RegistryPolicy::verified(), - ) - .await; - Ok(observed - .into_iter() - .zip(receipt.outcomes()) - .filter_map(|(observed, outcome)| { - outcome.as_ref().ok().map(|_| observed.event().clone()) - }) - .collect()) - } - - async fn publish(&self, authored: SocialDraft) -> Result<PublishReceipt> { - use radroots_event::contract::AuthorRole; - use radroots_event_codec::authoring::AuthoredEventPlan; - use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy}; - use radroots_storage::journal::IdempotencyKey; - use radroots_sync::{PushRequest, policy::SyncId}; - use radroots_transport::policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}; - - let identity = self.identity()?; - let targets = self - .nostr()? - .targets() - .ok_or_else(Error::shared_operation_unavailable)?; - let operation_uuid = uuid::Uuid::new_v4(); - let operation_id = - SyncId::new(*operation_uuid.as_bytes()).map_err(Error::invalid_host_configuration)?; - let now = now_unix_ms()?; - let created_at = now / 1_000; - let plan = match authored { - SocialDraft::Profile(profile) => { - AuthoredEventPlan::from_profile(&profile, created_at, identity.public_key_hex()) - } - SocialDraft::Update(update) => { - AuthoredEventPlan::from_update(&update, created_at, identity.public_key_hex()) - } - SocialDraft::Reply(reply) => { - AuthoredEventPlan::from_nip10_reply(&reply, created_at, identity.public_key_hex()) - } - } - .map_err(Error::invalid_host_configuration)?; - let actor = Actor::new( - identity.public_key(), - ActorSource::ExplicitPublicKey, - [AuthorRole::Any], - ) - .map_err(Error::invalid_host_configuration)?; - let request = PushRequest::new( - operation_id, - IdempotencyKey::parse(format!("sdk-{operation_uuid}")) - .map_err(Error::invalid_host_configuration)?, - actor, - plan, - targets, - SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), - now.checked_add(30_000) - .ok_or_else(Error::invalid_host_configuration_without_source)?, - CancellationPolicy::PreservePublishedRequest, - ) - .map_err(Error::invalid_host_configuration)?; - let sync = self - .client - .sync()? - .ok_or_else(Error::shared_operation_unavailable)?; - let preparation = sync - .prepare_push(request.clone()) - .await - .map_err(Error::shared_operation_failed)?; - sync.sign_prepared(request) - .await - .map_err(Error::shared_operation_failed)?; - sync.admit_signed(operation_id) - .await - .map_err(Error::shared_operation_failed)?; - let status = sync - .push_status(operation_id) - .await - .map_err(Error::shared_operation_failed)? - .ok_or_else(Error::shared_operation_failed_without_source)?; - let event_id = status - .artifact() - .signed() - .ok_or_else(Error::shared_operation_failed_without_source)? - .event() - .id_hex(); - let delivery = sync - .deliver_push(operation_id) - .await - .map_err(Error::shared_operation_failed)?; - Ok(PublishReceipt { - event_id, - replay: preparation.is_replay(), - delivery_state: delivery.plan().state(), - }) - } - - fn identity(&self) -> Result<crate::signing::LocalIdentity> { - self.client - .inner - .signing_slot - .as_ref() - .and_then(crate::signing::Slot::identity) - .ok_or_else(Error::shared_operation_unavailable) - } - - fn nostr(&self) -> Result<&crate::transport::NostrSlot> { - self.client - .inner - .nostr_slot - .as_ref() - .ok_or_else(Error::shared_operation_unavailable) - } -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -fn now_unix_ms() -> Result<u64> { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .ok() - .and_then(|duration| u64::try_from(duration.as_millis()).ok()) - .filter(|value| *value != 0) - .ok_or_else(Error::shared_operation_unavailable) -} - -#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] -fn profile_event( - event: &radroots_event::SignedEvent, -) -> std::result::Result< - ProfileEvent, - radroots_event_codec::decode::profile::RadrootsProfileMetadataParseError, -> { - let profile = - radroots_event_codec::decode::profile::parse_inbound_profile_metadata(event.content())?; - Ok(ProfileEvent { - event_id: event.id_hex(), - author: event.pubkey().to_hex(), - created_at: event.created_at(), - name: profile.name().map(str::to_owned), - display_name: profile.display_name().map(str::to_owned), - about: profile.about().map(str::to_owned), - picture: profile.picture().map(|value| value.as_str().to_owned()), - banner: profile.banner().map(|value| value.as_str().to_owned()), - nip05: profile.nip05().map(|value| value.as_str().to_owned()), - bot: profile.bot(), - }) -} - struct CloseAttempt { inner: Arc<ClientInner>, completed: bool, @@ -1085,17 +538,18 @@ mod tests { } #[test] - fn missing_storage_and_signer_without_sink_fail_closed() { + fn missing_storage_fails_and_signer_only_composition_is_valid() { assert!(matches!( ClientBuilder::new().build(), Err(error) if error.kind() == crate::error::ErrorKind::MissingStorage )); - assert!(matches!( - ClientBuilder::memory(generation()) - .signer(Arc::new(TestSigner)) - .build(), - Err(error) if error.kind() == crate::error::ErrorKind::SignerWithoutSink - )); + let signer_only = ClientBuilder::memory(generation()) + .signer(Arc::new(TestSigner)) + .build() + .expect("HTTP-only signing does not require a relay sink"); + assert!(signer_only.signer().expect("signer").is_some()); + assert!(signer_only.signing().expect("signing operations").is_some()); + assert!(signer_only.sink().expect("sink").is_none()); } #[test] @@ -1134,88 +588,9 @@ mod tests { ); } - #[cfg(all( - feature = "sync", - feature = "nostr", - feature = "local-signing", - feature = "nip46" - ))] + #[cfg(all(feature = "sync", feature = "nip46"))] #[test] - fn curated_models_accessors_and_builder_modes_are_complete() { - let profile_draft = ProfileDraft::new("farm") - .with_display_name("Farm") - .with_about("Local food") - .with_nip05("farm@example.test") - .with_bot(false); - assert_eq!(profile_draft.name, "farm"); - assert_eq!(profile_draft.display_name.as_deref(), Some("Farm")); - assert_eq!(profile_draft.about.as_deref(), Some("Local food")); - assert_eq!(profile_draft.nip05.as_deref(), Some("farm@example.test")); - assert_eq!(profile_draft.bot, Some(false)); - - let profile = ProfileEvent { - event_id: "event".to_owned(), - author: "author".to_owned(), - created_at: 7, - name: Some("farm".to_owned()), - display_name: Some("Farm".to_owned()), - about: Some("Local food".to_owned()), - picture: Some("https://example.test/picture".to_owned()), - banner: Some("https://example.test/banner".to_owned()), - nip05: Some("farm@example.test".to_owned()), - bot: Some(false), - }; - assert_eq!(profile.event_id(), "event"); - assert_eq!(profile.author(), "author"); - assert_eq!(profile.created_at(), 7); - assert_eq!(profile.name(), Some("farm")); - assert_eq!(profile.display_name(), Some("Farm")); - assert_eq!(profile.about(), Some("Local food")); - assert_eq!(profile.picture(), Some("https://example.test/picture")); - assert_eq!(profile.banner(), Some("https://example.test/banner")); - assert_eq!(profile.nip05(), Some("farm@example.test")); - assert_eq!(profile.bot(), Some(false)); - - let post = PostEvent { - event_id: "post".to_owned(), - author: "author".to_owned(), - created_at: 8, - content: "content".to_owned(), - }; - assert_eq!(post.event_id(), "post"); - assert_eq!(post.author(), "author"); - assert_eq!(post.created_at(), 8); - assert_eq!(post.content(), "content"); - - for (delivery_state, delivered, pending) in [ - (AuthoredDeliveryState::Pending, false, true), - (AuthoredDeliveryState::Retryable, false, true), - (AuthoredDeliveryState::Satisfied, true, false), - (AuthoredDeliveryState::Exhausted, false, false), - (AuthoredDeliveryState::FailedTerminal, false, false), - (AuthoredDeliveryState::Cancelled, false, false), - ] { - let receipt = PublishReceipt { - event_id: "published".to_owned(), - replay: true, - delivery_state, - }; - assert_eq!(receipt.event_id(), "published"); - assert!(receipt.is_replay()); - assert_eq!(receipt.delivery_state(), delivery_state); - assert_eq!(receipt.is_delivered(), delivered); - assert_eq!(receipt.is_delivery_pending(), pending); - } - - let health = TransportHealth { - configured: true, - source_available: true, - sink_available: false, - }; - assert!(health.is_configured()); - assert!(health.is_source_available()); - assert!(!health.is_sink_available()); - + fn canonical_operation_accessors_and_builder_modes_are_complete() { let builder = ClientBuilder::memory_default() .source(Arc::new(TestSource)) .host_sync(crate::sync::HostPolicy::default()); @@ -1225,7 +600,6 @@ mod tests { assert!(client.farm().expect("farm").is_some()); assert!(client.listing().expect("listing").is_some()); assert!(client.trade().expect("trade").is_some()); - assert!(client.social().is_err()); assert!( format!( "{:?}", @@ -1239,7 +613,6 @@ mod tests { ); let host = ClientBuilder::memory_default() - .sink(Arc::new(TestSink)) .signing(crate::signing::Provider::host(Arc::new(TestSigner))) .build() .expect("host signer"); @@ -1252,7 +625,6 @@ mod tests { ); let nip46 = ClientBuilder::memory_default() - .sink(Arc::new(TestSink)) .signing(crate::signing::Provider::nip46(Arc::new(TestSigner))) .build() .expect("nip46 signer"); @@ -1283,210 +655,6 @@ mod tests { assert_eq!(status.availability(), Availability::Available); } - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - #[tokio::test] - async fn shared_mobile_composition_is_single_client_explicit_and_fail_closed() { - let signing = crate::signing::Slot::new(); - let nostr = crate::transport::NostrSlot::new(crate::transport::RelayUrlPolicy::Local); - let client = ClientBuilder::memory(generation()) - .signing(crate::signing::Provider::slot(signing.clone())) - .nostr(nostr.clone()) - .host_sync(crate::sync::HostPolicy::standard()) - .build() - .expect("shared client"); - - let health = client - .social() - .expect("social composition") - .transport_health() - .await - .expect("passive health"); - assert!(!health.is_configured()); - assert!(!health.is_source_available()); - assert!(!health.is_sink_available()); - assert!(matches!( - client - .social() - .expect("social composition") - .fetch_posts(1, None) - .await, - Err(error) if error.kind() == crate::error::ErrorKind::SharedOperationUnavailable - )); - assert!( - client - .social() - .expect("social composition") - .fetch_profile_for_signer() - .await - .is_err() - ); - - let (_secret, identity) = signing.generate().expect("host key handoff"); - assert_eq!(signing.identity(), Some(identity)); - let social = client.social().expect("social composition"); - assert!(social.fetch_profile_for_signer().await.is_err()); - assert!(social.fetch_posts(2, Some(1)).await.is_err()); - assert!( - social - .publish_profile( - ProfileDraft::new("farm") - .with_display_name("Farm") - .with_about("Local food") - .with_nip05("farm@example.test") - .with_bot(false), - ) - .await - .is_err() - ); - assert!( - social - .publish_profile(ProfileDraft::new("farm")) - .await - .is_err() - ); - assert!(social.publish_text("local update").await.is_err()); - assert!( - social - .publish_reply( - "local reply", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - Some("ws://127.0.0.1:7447"), - ) - .await - .is_err() - ); - assert!( - social - .publish_reply("local reply", "invalid", "invalid", None) - .await - .is_err() - ); - nostr - .configure(["ws://127.0.0.1:7447"]) - .expect("relay selection"); - assert!(nostr.targets().is_some()); - let social = client.social().expect("social composition"); - assert!( - social - .transport_health() - .await - .expect("configured passive health") - .is_configured() - ); - let fetch = tokio::time::timeout( - core::time::Duration::from_secs(3), - social.fetch_posts(2, Some(1)), - ) - .await - .expect("localhost fetch remains bounded"); - assert!(fetch.is_err() || fetch.is_ok_and(|events| events.is_empty())); - let profile_fetch = tokio::time::timeout( - core::time::Duration::from_secs(3), - social.fetch_profile_for_signer(), - ) - .await - .expect("localhost profile fetch remains bounded"); - assert!(profile_fetch.is_err() || profile_fetch.is_ok_and(|event| event.is_none())); - let publish = tokio::time::timeout( - core::time::Duration::from_secs(3), - social.publish_text("configured local update"), - ) - .await - .expect("localhost publish remains bounded"); - match publish { - Ok(receipt) => { - assert_eq!(receipt.event_id().len(), 64); - assert!(!receipt.is_replay()); - } - Err(error) => assert_eq!(error.kind(), crate::error::ErrorKind::SharedOperationFailed), - } - let profile_publish = tokio::time::timeout( - core::time::Duration::from_secs(3), - social.publish_profile( - ProfileDraft::new("configured-farm") - .with_display_name("Configured Farm") - .with_about("Local food"), - ), - ) - .await - .expect("localhost profile publish remains bounded"); - assert!( - profile_publish.is_ok() - || matches!( - profile_publish, - Err(error) - if error.kind() == crate::error::ErrorKind::SharedOperationFailed - ) - ); - let reply_publish = tokio::time::timeout( - core::time::Duration::from_secs(3), - social.publish_reply( - "configured reply", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - None, - ), - ) - .await - .expect("localhost reply publish remains bounded"); - assert!( - reply_publish.is_ok() - || matches!( - reply_publish, - Err(error) - if error.kind() == crate::error::ErrorKind::SharedOperationFailed - ) - ); - nostr.clear(); - assert!(nostr.targets().is_none()); - signing.clear(); - assert!(signing.identity().is_none()); - assert_eq!( - radroots_signing::Signer::status(&signing) - .await - .expect("empty slot status") - .availability(), - radroots_signing::status::SignerAvailability::Unavailable - ); - } - - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - #[test] - fn verified_profile_projection_preserves_the_curated_public_fields() { - use radroots_event::wire::v1::Nip01EventWire; - - let author = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; - let content = r#"{"name":"farm","display_name":"Farm","about":"Local food","nip05":"farm@example.test","bot":false}"#; - let id = radroots_event::draft::compute_nip01_event_id(author, 7, 0, &[], content) - .expect("canonical profile id") - .to_hex(); - let raw = serde_json::json!({ - "id": id, - "pubkey": author, - "created_at": 7, - "kind": 0, - "tags": [], - "content": content, - "sig": "d".repeat(128), - }) - .to_string(); - let wire = Nip01EventWire::parse_json(&raw).expect("profile wire"); - let event = radroots_event::SignedEvent::from_wire_verified_id(wire, raw) - .expect("verified profile event"); - let profile = profile_event(&event).expect("profile projection"); - assert_eq!(profile.event_id(), id); - assert_eq!(profile.author(), author); - assert_eq!(profile.created_at(), 7); - assert_eq!(profile.name(), Some("farm")); - assert_eq!(profile.display_name(), Some("Farm")); - assert_eq!(profile.about(), Some("Local food")); - assert_eq!(profile.nip05(), Some("farm@example.test")); - assert_eq!(profile.bot(), Some(false)); - assert_eq!(profile.picture(), None); - assert_eq!(profile.banner(), None); - } - #[test] fn close_is_clone_shared_idempotent_and_rejects_later_capability_access() { let client = ClientBuilder::memory(generation()).build().expect("client"); diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -71,13 +71,6 @@ error_catalog! { message: "SDK storage capability is not configured", safe_detail_keys: [] }, - SignerWithoutSink => { - code: SignerWithoutSink, - operation: None, - capability: None, - message: "SDK signer requires an outbound event sink", - safe_detail_keys: [] - }, CloseInProgress => { code: ClientCloseInProgress, operation: None, @@ -155,13 +148,6 @@ error_catalog! { message: "SDK shared network operation is unavailable", safe_detail_keys: [] }, - SharedOperationFailed => { - code: SyncPartial, - operation: None, - capability: None, - message: "SDK shared network operation failed", - safe_detail_keys: [] - }, } /// Stable metadata for one native SDK failure. @@ -242,10 +228,6 @@ impl Error { Self::without_source(ErrorKind::MissingStorage) } - pub(crate) fn signer_without_sink() -> Self { - Self::without_source(ErrorKind::SignerWithoutSink) - } - pub(crate) fn close_in_progress() -> Self { Self::without_source(ErrorKind::CloseInProgress) } @@ -321,21 +303,6 @@ impl Error { Self::without_source(ErrorKind::SharedOperationUnavailable) } - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - pub(crate) fn shared_operation_failed( - source: impl error::Error + Send + Sync + 'static, - ) -> Self { - Self { - kind: ErrorKind::SharedOperationFailed, - source: Some(Box::new(source)), - } - } - - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - pub(crate) fn shared_operation_failed_without_source() -> Self { - Self::without_source(ErrorKind::SharedOperationFailed) - } - fn without_source(kind: ErrorKind) -> Self { Self { kind, source: None } } @@ -450,7 +417,6 @@ mod tests { fn native_constructor_and_descriptor_surface_is_complete() { let source_free = [ Error::missing_storage(), - Error::signer_without_sink(), Error::close_in_progress(), Error::client_closing(), Error::client_closed(), @@ -491,17 +457,5 @@ mod tests { .source() .is_none() ); - - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - { - let failed = Error::shared_operation_failed(std::io::Error::other("private")); - assert_eq!(failed.kind(), ErrorKind::SharedOperationFailed); - assert!(failed.source().is_some()); - assert!( - Error::shared_operation_failed_without_source() - .source() - .is_none() - ); - } } } diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs @@ -1,10 +1,19 @@ //! Generic signer composition without protocol or relay ownership. -use std::sync::Arc; -#[cfg(feature = "local-signing")] -use std::sync::RwLock; +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; -use radroots_signing::{SignReceipt, SignRequest, Signer, SignerStatus}; +#[cfg(feature = "blossom")] +use radroots_signing::SigningPurpose; +use radroots_signing::{ + Actor, SignReceipt, SignRequest, Signer, SignerStatus, SigningIntentId, request::SignPolicy, +}; + +pub use radroots_event_codec::authoring::BlossomAuthorizationPlan; +#[cfg(feature = "blossom")] +pub use radroots_nostr::blossom::AuthorizationHeader; /// Host-visible signer composition mode. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -23,8 +32,97 @@ pub enum Mode { pub struct Provider { mode: Mode, signer: Arc<dyn Signer>, - #[cfg(feature = "local-signing")] - slot: Option<Slot>, +} + +/// Borrowed high-level signing operations over one configured opaque signer. +#[derive(Clone, Copy)] +pub struct Operations<'a> { + signer: &'a dyn Signer, +} + +impl<'a> Operations<'a> { + pub(crate) const fn new(signer: &'a dyn Signer) -> Self { + Self { signer } + } + + /// Delegates an already authorized exact request to the opaque signer. + pub async fn sign(&self, request: SignRequest) -> Result<SignReceipt, radroots_signing::Error> { + sign_checked(self.signer, request).await + } + + /// Signs one HTTP-only BUD-11 upload plan and returns its canonical header. + #[cfg(feature = "blossom")] + pub async fn authorize_blossom_upload( + &self, + request: SignRequest, + ) -> Result<radroots_nostr::blossom::AuthorizationHeader, BlossomSigningError> { + if request.purpose() != SigningPurpose::BlossomUploadAuthorization { + return Err(BlossomSigningError::WrongPurpose); + } + let receipt = self + .sign(request) + .await + .map_err(BlossomSigningError::Signing)?; + radroots_nostr::blossom::encode_signed_event_authorization_header(receipt.signed_event()) + .map_err(BlossomSigningError::Encoding) + } +} + +impl std::fmt::Debug for Operations<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Operations") + .field("signer", &"<borrowed opaque signer>") + .finish() + } +} + +/// Creates one domain-separated BUD-11 request from an exact HTTP-only plan. +pub fn blossom_upload_request( + operation_kind: radroots_protocol::runtime::v1::OperationId, + intent_id: SigningIntentId, + actor: Actor, + plan: BlossomAuthorizationPlan, + policy: SignPolicy, +) -> Result<SignRequest, radroots_signing::Error> { + SignRequest::blossom_upload(operation_kind, intent_id, actor, plan, policy) +} + +/// Failure while producing a BUD-11 HTTP authorization header. +#[cfg(feature = "blossom")] +#[derive(Debug)] +#[non_exhaustive] +pub enum BlossomSigningError { + /// The caller supplied a relay-authoring request to the HTTP-only method. + WrongPurpose, + /// The opaque signer rejected or failed the exact request. + Signing(radroots_signing::Error), + /// The verified signer result could not be encoded as BUD-11. + Encoding(radroots_nostr::blossom::AuthorizationError), +} + +#[cfg(feature = "blossom")] +impl std::fmt::Display for BlossomSigningError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::WrongPurpose => { + formatter.write_str("signing request is not BUD-11 HTTP authorization") + } + Self::Signing(error) => error.fmt(formatter), + Self::Encoding(error) => error.fmt(formatter), + } + } +} + +#[cfg(feature = "blossom")] +impl std::error::Error for BlossomSigningError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::WrongPurpose => None, + Self::Signing(error) => Some(error), + Self::Encoding(error) => Some(error), + } + } } impl Provider { @@ -34,8 +132,6 @@ impl Provider { Self { mode: Mode::Host, signer, - #[cfg(feature = "local-signing")] - slot: None, } } @@ -46,21 +142,6 @@ impl Provider { Self { mode: Mode::Local, signer: Arc::new(signer), - slot: None, - } - } - - /// Wraps a host-controlled local signer slot. - /// - /// The slot starts inert and can be populated or cleared without rebuilding - /// the client. Secret persistence remains entirely host-owned. - #[cfg(feature = "local-signing")] - #[must_use] - pub fn slot(slot: Slot) -> Self { - Self { - mode: Mode::Local, - signer: Arc::new(slot.clone()), - slot: Some(slot), } } @@ -75,8 +156,6 @@ impl Provider { Self { mode: Mode::Nip46, signer, - #[cfg(feature = "local-signing")] - slot: None, } } @@ -99,196 +178,38 @@ impl Provider { /// Delegates one already-authorized request to the canonical SPI. pub async fn sign(&self, request: SignRequest) -> Result<SignReceipt, radroots_signing::Error> { - self.signer.sign(request).await + sign_checked(self.signer.as_ref(), request).await } - #[cfg(feature = "local-signing")] - pub(crate) fn into_parts(self) -> (Arc<dyn Signer>, Option<Slot>) { - (self.signer, self.slot) - } - - #[cfg(not(feature = "local-signing"))] pub(crate) fn into_signer(self) -> Arc<dyn Signer> { self.signer } } -/// Public identity controlled by an installed local signer. -#[cfg(feature = "local-signing")] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct LocalIdentity { - public_key: radroots_identity::PublicKey, - npub: String, +async fn sign_checked( + signer: &dyn Signer, + request: SignRequest, +) -> Result<SignReceipt, radroots_signing::Error> { + let expected = request.clone(); + let returned = signer.sign(request).await?; + SignReceipt::from_signed_event( + &expected, + returned.signed_event().clone(), + system_time_unix_ms()?, + ) } -#[cfg(feature = "local-signing")] -impl LocalIdentity { - fn from_public_key( - public_key: radroots_identity::PublicKey, - ) -> Result<Self, radroots_nostr::Error> { - Ok(Self { - public_key, - npub: radroots_nostr::key::public_key_to_npub(public_key)?, - }) - } - - /// Returns the canonical lowercase public-key hexadecimal form. - #[must_use] - pub fn public_key_hex(&self) -> String { - self.public_key.to_hex() - } - - /// Returns the canonical NIP-19 public identity. - #[must_use] - pub fn npub(&self) -> &str { - self.npub.as_str() - } - - #[cfg(any(test, all(feature = "sync", feature = "nostr")))] - pub(crate) const fn public_key(&self) -> radroots_identity::PublicKey { - self.public_key - } -} - -/// Mutable, client-shareable local signer selected by the host. -/// -/// The slot never persists key material and its debug output never observes -/// the installed signer. Installing and clearing are explicit host actions. -#[cfg(feature = "local-signing")] -#[derive(Clone, Default)] -pub struct Slot { - state: Arc<RwLock<Option<SlotState>>>, -} - -#[cfg(feature = "local-signing")] -struct SlotState { - signer: Arc<dyn Signer>, - identity: LocalIdentity, -} - -#[cfg(feature = "local-signing")] -impl Slot { - /// Creates an empty signer slot. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Validates and installs one host-supplied hexadecimal or `nsec` secret. - pub fn install(&self, encoded: &str) -> Result<LocalIdentity, radroots_nostr::Error> { - let secret = radroots_nostr::key::SecretKey::parse(encoded)?; - self.install_secret(secret) - } - - /// Generates, installs, and returns one secret for immediate host custody. - /// - /// The SDK retains only the opaque signer. The returned `nsec` is the sole - /// persistence handoff and must be moved into host secure storage. - pub fn generate(&self) -> Result<(String, LocalIdentity), radroots_nostr::Error> { - let secret = radroots_nostr::key::SecretKey::generate(); - let encoded = radroots_nostr::key::secret_key_to_nsec(&secret); - let identity = self.install_secret(secret)?; - Ok((encoded, identity)) - } - - /// Removes the active signer from this process. - pub fn clear(&self) { - if let Ok(mut state) = self.state.write() { - *state = None; - } - } - - /// Returns the currently installed public identity. - #[must_use] - pub fn identity(&self) -> Option<LocalIdentity> { - self.state - .read() - .ok() - .and_then(|state| state.as_ref().map(|state| state.identity.clone())) - } - - fn install_secret( - &self, - secret: radroots_nostr::key::SecretKey, - ) -> Result<LocalIdentity, radroots_nostr::Error> { - let public_key = secret.public_key()?; - let identity = LocalIdentity::from_public_key(public_key)?; - let signer: Arc<dyn Signer> = Arc::new(radroots_nostr::signing::LocalSigner::new(secret)?); - if let Ok(mut state) = self.state.write() { - *state = Some(SlotState { - signer, - identity: identity.clone(), - }); - } - Ok(identity) - } - - fn signer(&self) -> Result<Arc<dyn Signer>, radroots_signing::Error> { - self.state - .read() - .map_err(|source| { - radroots_signing::Error::with_source( - radroots_signing::error::Kind::InternalError, - LockFailure(source.to_string()), - ) - })? - .as_ref() - .map(|state| Arc::clone(&state.signer)) - .ok_or_else(|| { - radroots_signing::Error::new(radroots_signing::error::Kind::SignerUnavailable) +fn system_time_unix_ms() -> Result<u64, radroots_signing::Error> { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| radroots_signing::Error::new(radroots_signing::error::Kind::InternalError)) + .and_then(|duration| { + u64::try_from(duration.as_millis()).map_err(|_| { + radroots_signing::Error::new(radroots_signing::error::Kind::InternalError) }) - } -} - -#[cfg(feature = "local-signing")] -impl Signer for Slot { - fn status( - &self, - ) -> radroots_signing::signer::BoxFuture<'_, Result<SignerStatus, radroots_signing::Error>> - { - Box::pin(async move { - match self.signer() { - Ok(signer) => signer.status().await, - Err(error) if error.kind() == radroots_signing::error::Kind::SignerUnavailable => { - Ok(SignerStatus::unavailable()) - } - Err(error) => Err(error), - } }) - } - - fn sign( - &self, - request: SignRequest, - ) -> radroots_signing::signer::BoxFuture<'_, Result<SignReceipt, radroots_signing::Error>> { - Box::pin(async move { self.signer()?.sign(request).await }) - } } -#[cfg(feature = "local-signing")] -impl std::fmt::Debug for Slot { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("Slot") - .field("installed", &self.identity().is_some()) - .finish() - } -} - -#[cfg(feature = "local-signing")] -#[derive(Debug)] -struct LockFailure(String); - -#[cfg(feature = "local-signing")] -impl std::fmt::Display for LockFailure { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(self.0.as_str()) - } -} - -#[cfg(feature = "local-signing")] -impl std::error::Error for LockFailure {} - impl std::fmt::Debug for Provider { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter @@ -330,6 +251,8 @@ mod tests { use super::*; const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + #[cfg(feature = "local-signing")] + const SECRET_KEY: &str = "7e0112ad58b2d2d13fb80532625195dc169b86d72b0e1db48347837a785cae90"; struct ScriptedSigner { status: SignerStatus, @@ -353,9 +276,9 @@ mod tests { } } - fn request() -> SignRequest { + fn request_for(public_key: PublicKey, artifact: u8, deadline_unix_ms: u64) -> SignRequest { let actor = Actor::new( - PublicKey::from_hex(PUBLIC_KEY).expect("public key"), + public_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any], ) @@ -367,7 +290,7 @@ mod tests { 1_700_000_000, Vec::new(), "frozen-content", - PUBLIC_KEY, + public_key.to_hex(), ) .expect("draft"), ) @@ -376,16 +299,69 @@ mod tests { OperationId::SyncPush, SigningIntentId::new( SigningOperationId::new([1; 16]).expect("operation id"), - AuthoredArtifactId::new([2; 16]).expect("artifact id"), + AuthoredArtifactId::new([artifact; 16]).expect("artifact id"), ), actor, plan, - SignPolicy::new(1_700_000_100, CancellationPolicy::PreservePublishedRequest) - .expect("policy"), + SignPolicy::new( + deadline_unix_ms, + CancellationPolicy::PreservePublishedRequest, + ) + .expect("policy"), ) .expect("request") } + fn request() -> SignRequest { + request_for( + PublicKey::from_hex(PUBLIC_KEY).expect("public key"), + 2, + 1_700_000_100, + ) + } + + #[cfg(feature = "local-signing")] + fn local_signer() -> radroots_nostr::signing::LocalSigner { + radroots_nostr::signing::LocalSigner::new( + radroots_nostr::key::SecretKey::parse(SECRET_KEY).expect("secret fixture"), + ) + .expect("local signer") + } + + #[cfg(all(feature = "local-signing", feature = "blossom"))] + fn blossom_request_for(public_key: PublicKey) -> SignRequest { + use radroots_blossom::{ + Sha256, + authorization::{AuthoredUploadClaim, AuthorizationContent, ServerDomain}, + }; + + let claim = AuthoredUploadClaim::new( + AuthorizationContent::parse("Upload exact SDK image").expect("content"), + ServerDomain::parse("media.example").expect("server"), + Sha256::digest(b"exact-sdk-image"), + 1_700_000_000, + 60, + ) + .expect("claim"); + let actor = Actor::new( + public_key, + ActorSource::ExplicitPublicKey, + [AuthorRole::Any], + ) + .expect("actor"); + blossom_upload_request( + OperationId::SyncPush, + SigningIntentId::new( + SigningOperationId::new([9; 16]).expect("operation id"), + AuthoredArtifactId::new([10; 16]).expect("artifact id"), + ), + actor, + BlossomAuthorizationPlan::for_upload(&claim, public_key).expect("plan"), + SignPolicy::new(u64::MAX, CancellationPolicy::LocalCooperative).expect("policy"), + ) + .expect("Blossom request") + } + #[cfg(feature = "nip46")] fn remote_status(progress: Option<SignProgress>) -> SignerStatus { SignerStatus::new( @@ -412,61 +388,75 @@ mod tests { assert_eq!(status.capabilities()[0].kind(), SignerKind::Local); } - #[cfg(feature = "local-signing")] + #[cfg(all(feature = "local-signing", feature = "blossom"))] #[tokio::test] - async fn local_slot_hands_secret_to_host_and_supports_lock_restore() { - let slot = Slot::new(); - assert!(slot.identity().is_none()); - assert_eq!( - slot.status().await.expect("empty status").availability(), - SignerAvailability::Unavailable - ); - - let (secret, generated) = slot.generate().expect("generated identity"); - assert!(secret.starts_with("nsec1")); - assert_eq!(slot.identity().expect("installed"), generated); - assert!(!format!("{slot:?}").contains(secret.as_str())); + async fn focused_operations_sign_exact_events_and_bud11_without_secret_surface() { + use radroots_blossom::{ + Sha256, + authorization::{AuthorizationTarget, AuthorizationValidation, ServerDomain}, + }; - slot.clear(); - assert!(slot.identity().is_none()); - let restored = slot.install(secret.as_str()).expect("restored identity"); - assert_eq!(restored, generated); - assert_eq!(restored.public_key_hex(), restored.public_key().to_hex()); - assert!(restored.npub().starts_with("npub1")); + let signer = local_signer(); + let public_key = signer.public_key(); + let provider = Provider::local(signer); + let operations = Operations::new(provider.as_signer()); + assert!(format!("{provider:?}").contains("signer: \"<opaque>\"")); + assert!(format!("{operations:?}").contains("borrowed opaque signer")); + + let receipt = operations + .sign(request_for(public_key, 3, u64::MAX)) + .await + .expect("focused sign"); + assert_eq!(receipt.signed_event().pubkey(), &public_key); + let provider_receipt = provider + .sign(request_for(public_key, 4, u64::MAX)) + .await + .expect("provider sign"); + assert_eq!(provider_receipt.signed_event().pubkey(), &public_key); + + let wrong_purpose = operations + .authorize_blossom_upload(request_for(public_key, 5, u64::MAX)) + .await + .expect_err("relay event cannot become an HTTP credential"); + assert!(matches!(wrong_purpose, BlossomSigningError::WrongPurpose)); + + let header = operations + .authorize_blossom_upload(blossom_request_for(public_key)) + .await + .expect("BUD-11 authorization"); + let hash = Sha256::digest(b"exact-sdk-image"); + let verified = radroots_nostr::blossom::decode_verify_authorization_header( + header.as_str(), + &AuthorizationValidation::bud11( + AuthorizationTarget::Upload(hash), + ServerDomain::parse("media.example").expect("server"), + 1_700_000_001, + ), + ) + .expect("verify BUD-11 header"); + assert_eq!(verified.author().to_hex(), public_key.to_hex()); + } - let provider = Provider::slot(slot.clone()); - assert_eq!(provider.mode(), Mode::Local); - assert!(format!("{provider:?}").contains("<opaque>")); - let (_signer, provider_slot) = provider.into_parts(); - assert!(provider_slot.is_some()); + #[cfg(feature = "blossom")] + #[test] + fn blossom_errors_are_typed_and_preserve_only_explicit_sources() { + use std::error::Error as _; - slot.clear(); - assert_eq!( - slot.sign(request()).await.expect_err("empty slot").kind(), - Kind::SignerUnavailable - ); - } + let wrong = BlossomSigningError::WrongPurpose; + assert!(wrong.source().is_none()); + assert!(!wrong.to_string().is_empty()); - #[cfg(feature = "local-signing")] - #[tokio::test] - async fn poisoned_local_slot_fails_closed_without_exposing_key_state() { - let slot = Slot::new(); - let state = Arc::clone(&slot.state); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _guard = state.write().expect("write lock"); - panic!("poison signer slot"); - })); + let signing = BlossomSigningError::Signing(Error::new(Kind::SignerRejected)); + assert!(signing.source().is_some()); + assert_eq!(signing.to_string(), "signer rejected the request"); - slot.clear(); - assert!(slot.identity().is_none()); - let (_secret, identity) = slot.generate().expect("secret remains host-owned"); - assert!(!identity.public_key_hex().is_empty()); + let encoding = BlossomSigningError::Encoding( + radroots_nostr::blossom::AuthorizationError::InvalidEventSignature, + ); + assert!(encoding.source().is_some()); assert_eq!( - slot.sign(request()) - .await - .expect_err("poisoned slot") - .kind(), - Kind::InternalError + encoding.to_string(), + "invalid Blossom authorization event signature" ); } diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -23,6 +23,7 @@ fn manifest_has_final_identity_and_dependency_boundary() { let dependencies = dependency_names(MANIFEST); let expected = BTreeSet::from([ + "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", @@ -72,6 +73,7 @@ fn manifest_has_exact_feature_vocabulary_and_explicit_optional_activation() { assert_eq!( features, BTreeSet::from([ + "blossom", "default", "full", "geonames", diff --git a/crates/sdk/tests/public_api.rs b/crates/sdk/tests/public_api.rs @@ -38,7 +38,9 @@ fn public_native_type_snapshot_uses_contextual_names() { type_name::<radroots_sdk::listing::PrepareErrorKind>(), type_name::<radroots_sdk::listing::PrepareRequest>(), type_name::<radroots_sdk::signing::Mode>(), + type_name::<radroots_sdk::signing::Operations<'static>>(), type_name::<radroots_sdk::signing::Provider>(), + type_name::<radroots_sdk::signing::BlossomAuthorizationPlan>(), type_name::<radroots_sdk::storage::Operations<'static>>(), type_name::<radroots_sdk::trade::Plan>(), type_name::<radroots_sdk::trade::PrepareError>(), @@ -61,29 +63,17 @@ fn public_native_type_snapshot_uses_contextual_names() { type_name::<radroots_sdk::trade::PrivateTermsError>(), ]) .collect::<BTreeSet<_>>(); - #[cfg(feature = "local-signing")] - let actual = actual - .into_iter() - .chain([ - type_name::<radroots_sdk::signing::LocalIdentity>(), - type_name::<radroots_sdk::signing::Slot>(), - ]) - .collect::<BTreeSet<_>>(); #[cfg(feature = "nostr")] let actual = actual .into_iter() .chain([type_name::<radroots_sdk::transport::NostrSlot>()]) .collect::<BTreeSet<_>>(); - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + #[cfg(feature = "blossom")] let actual = actual .into_iter() .chain([ - type_name::<radroots_sdk::client::PostEvent>(), - type_name::<radroots_sdk::client::ProfileDraft>(), - type_name::<radroots_sdk::client::ProfileEvent>(), - type_name::<radroots_sdk::client::PublishReceipt>(), - type_name::<radroots_sdk::client::SocialOperations<'static>>(), - type_name::<radroots_sdk::client::TransportHealth>(), + type_name::<radroots_sdk::signing::AuthorizationHeader>(), + type_name::<radroots_sdk::signing::BlossomSigningError>(), ]) .collect::<BTreeSet<_>>(); #[cfg(feature = "radrootsd")] @@ -157,15 +147,13 @@ fn active_native_api_has_no_sdk_owned_traits_or_public_field_layout() { } const fn expected_public_type_count() -> usize { - let count = 28; + let count = 30; #[cfg(feature = "sync")] let count = count + 9; - #[cfg(feature = "local-signing")] - let count = count + 2; #[cfg(feature = "nostr")] let count = count + 1; - #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] - let count = count + 6; + #[cfg(feature = "blossom")] + let count = count + 2; #[cfg(feature = "radrootsd")] let count = count + 5; count diff --git a/crates/signing/Cargo.toml b/crates/signing/Cargo.toml @@ -59,6 +59,9 @@ serde = { workspace = true, default-features = false, features = [ ], optional = true } [dev-dependencies] +radroots_blossom = { workspace = true, default-features = false, features = [ + "std", +] } serde_json = { workspace = true, features = ["std"] } nostr = { workspace = true, features = ["std"] } diff --git a/crates/signing/README.md b/crates/signing/README.md @@ -26,6 +26,12 @@ The authoritative package charter is the 5. The implementation creates a [`SignReceipt`] from the originating request. Receipt construction rejects plan drift and invalid Schnorr signatures. +The same SPI supports a purpose-tagged, HTTP-only +`BlossomAuthorizationPlan`. `SigningPurpose` keeps that BUD-11 credential +distinct from registry-authored relay events, while receipt verification still +binds the exact author, timestamp, kind, tags, content, event ID, deadline, and +cancellation signal. + [`Actor`]: crate::Actor [`Signer`]: crate::Signer [`SignRequest`]: crate::SignRequest @@ -136,6 +142,9 @@ secret material in it. authorization and never invokes a signer on failure. - A successful receipt proves exact equality of author, timestamp, kind, tags, content, and event ID with the exact request plan, plus a valid signature. +- Callers at a persistence or HTTP boundary must revalidate an untrusted host + signer's returned event against their retained request and locally observed + completion time before committing or transmitting it. - `Error` display/debug output and protocol reports are redacted. Under `std`, a caller may explicitly inspect a preserved native error source locally. - `AuthChallenge` debug output redacts its URI; the value accepts only bounded diff --git a/crates/signing/src/lib.rs b/crates/signing/src/lib.rs @@ -21,6 +21,6 @@ pub use authorization::{CurrentAuthoringAuthority, CurrentAuthoringDecision}; pub use error::Error; pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId}; pub use receipt::SignReceipt; -pub use request::SignRequest; +pub use request::{SignRequest, SigningPurpose}; pub use signer::Signer; pub use status::SignerStatus; diff --git a/crates/signing/src/receipt.rs b/crates/signing/src/receipt.rs @@ -83,13 +83,12 @@ impl SignReceipt { } fn verify_exact_plan(event: &SignedEvent, request: &SignRequest) -> Result<(), Error> { - let plan = request.plan(); - if event.pubkey() != plan.author() - || event.created_at() != plan.created_at() - || event.kind() != plan.body().kind() - || event.tags_as_vec() != plan.body().tags() - || event.content() != plan.body().content() - || event.id() != plan.expected_event_id() + if event.pubkey() != request.expected_author() + || event.created_at() != request.created_at() + || event.kind() != request.kind() + || event.tags_as_vec() != request.tags() + || event.content() != request.content() + || event.id() != request.expected_event_id() { return Err(Error::new(Kind::SignerOutputInvalid)); } diff --git a/crates/signing/src/request.rs b/crates/signing/src/request.rs @@ -5,7 +5,8 @@ use core::{ sync::atomic::{AtomicBool, Ordering}, }; use radroots_event::contract::event_contract; -use radroots_event_codec::authoring::AuthoredEventPlan; +use radroots_event_codec::authoring::{AuthoredEventPlan, BlossomAuthorizationPlan, PlanDigest}; +use radroots_identity::PublicKey; use radroots_protocol::runtime::v1::OperationId; #[cfg(not(feature = "std"))] @@ -143,6 +144,91 @@ pub trait ProgressObserver: Send + Sync { fn on_progress(&self, progress: &SignProgress); } +/// Domain-separated reason an exact Nostr event is being signed. +#[non_exhaustive] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SigningPurpose { + /// A registry-authorized event that may enter the durable relay pipeline. + AuthoredEvent, + /// A short-lived BUD-11 upload credential for an HTTP request only. + BlossomUploadAuthorization, +} + +#[derive(Clone)] +enum SigningPlan { + Authored(AuthoredEventPlan), + BlossomUpload(BlossomAuthorizationPlan), +} + +impl SigningPlan { + const fn purpose(&self) -> SigningPurpose { + match self { + Self::Authored(_) => SigningPurpose::AuthoredEvent, + Self::BlossomUpload(_) => SigningPurpose::BlossomUploadAuthorization, + } + } + + const fn author(&self) -> &PublicKey { + match self { + Self::Authored(plan) => plan.author(), + Self::BlossomUpload(plan) => plan.author(), + } + } + + const fn created_at(&self) -> u64 { + match self { + Self::Authored(plan) => plan.created_at(), + Self::BlossomUpload(plan) => plan.created_at(), + } + } + + const fn kind(&self) -> u32 { + match self { + Self::Authored(plan) => plan.body().kind(), + Self::BlossomUpload(plan) => plan.kind(), + } + } + + fn tags(&self) -> &[alloc_or_std::Vec<alloc_or_std::String>] { + match self { + Self::Authored(plan) => plan.body().tags(), + Self::BlossomUpload(plan) => plan.tags(), + } + } + + fn content(&self) -> &str { + match self { + Self::Authored(plan) => plan.body().content(), + Self::BlossomUpload(plan) => plan.content(), + } + } + + const fn expected_event_id(&self) -> &radroots_event::EventId { + match self { + Self::Authored(plan) => plan.expected_event_id(), + Self::BlossomUpload(plan) => plan.expected_event_id(), + } + } + + const fn digest(&self) -> PlanDigest { + match self { + Self::Authored(plan) => plan.digest(), + Self::BlossomUpload(plan) => plan.digest(), + } + } +} + +#[cfg(not(feature = "std"))] +mod alloc_or_std { + pub use alloc::{string::String, vec::Vec}; +} +#[cfg(feature = "std")] +mod alloc_or_std { + pub use std::{string::String, vec::Vec}; +} + /// One currently authorized exact plan and bounded signer invocation. #[derive(Clone)] pub struct SignRequest { @@ -150,8 +236,8 @@ pub struct SignRequest { intent_id: SigningIntentId, signer_request_id: SignerRequestId, actor: Actor, - plan: AuthoredEventPlan, - authorization: CurrentAuthoringDecision, + plan: SigningPlan, + authorization: Option<CurrentAuthoringDecision>, policy: SignPolicy, cancellation_signal: CancellationSignal, progress_observer: Option<Arc<dyn ProgressObserver>>, @@ -185,6 +271,33 @@ impl SignRequest { ) -> Result<Self, Error> { let authorization = authority.evaluate(&plan); authorize(&actor, &plan, policy, authorization)?; + let plan = SigningPlan::Authored(plan); + let signer_request_id = SignerRequestId::derive(intent_id.artifact_id(), plan.digest()); + Ok(Self { + operation_kind, + intent_id, + signer_request_id, + actor, + plan, + authorization: Some(authorization), + policy, + cancellation_signal: CancellationSignal::new(), + progress_observer: None, + }) + } + + /// Creates a bounded HTTP-only BUD-11 upload authorization request. + pub fn blossom_upload( + operation_kind: OperationId, + intent_id: SigningIntentId, + actor: Actor, + plan: BlossomAuthorizationPlan, + policy: SignPolicy, + ) -> Result<Self, Error> { + if actor.public_key() != *plan.author() || !policy.managed_signing().permits(&actor) { + return Err(Error::new(Kind::AuthorizationDenied)); + } + let plan = SigningPlan::BlossomUpload(plan); let signer_request_id = SignerRequestId::derive(intent_id.artifact_id(), plan.digest()); Ok(Self { operation_kind, @@ -192,7 +305,7 @@ impl SignRequest { signer_request_id, actor, plan, - authorization, + authorization: None, policy, cancellation_signal: CancellationSignal::new(), progress_observer: None, @@ -232,12 +345,65 @@ impl SignRequest { } #[must_use] - pub const fn plan(&self) -> &AuthoredEventPlan { - &self.plan + pub const fn purpose(&self) -> SigningPurpose { + self.plan.purpose() + } + + /// Returns the registry-authored plan, if this is a relay-event request. + #[must_use] + pub const fn authored_plan(&self) -> Option<&AuthoredEventPlan> { + match &self.plan { + SigningPlan::Authored(plan) => Some(plan), + SigningPlan::BlossomUpload(_) => None, + } + } + + /// Returns the HTTP-only upload-authorization plan, when applicable. + #[must_use] + pub const fn blossom_authorization_plan(&self) -> Option<&BlossomAuthorizationPlan> { + match &self.plan { + SigningPlan::Authored(_) => None, + SigningPlan::BlossomUpload(plan) => Some(plan), + } + } + + #[must_use] + pub const fn expected_author(&self) -> &PublicKey { + self.plan.author() + } + + #[must_use] + pub const fn created_at(&self) -> u64 { + self.plan.created_at() + } + + #[must_use] + pub const fn kind(&self) -> u32 { + self.plan.kind() + } + + #[must_use] + pub fn tags(&self) -> &[alloc_or_std::Vec<alloc_or_std::String>] { + self.plan.tags() + } + + #[must_use] + pub fn content(&self) -> &str { + self.plan.content() + } + + #[must_use] + pub const fn expected_event_id(&self) -> &radroots_event::EventId { + self.plan.expected_event_id() + } + + #[must_use] + pub const fn plan_digest(&self) -> PlanDigest { + self.plan.digest() } #[must_use] - pub const fn authorization(&self) -> CurrentAuthoringDecision { + pub const fn authorization(&self) -> Option<CurrentAuthoringDecision> { self.authorization } @@ -303,7 +469,8 @@ impl fmt::Debug for SignRequest { .field("intent_id", &self.intent_id) .field("signer_request_id", &self.signer_request_id) .field("actor", &self.actor) - .field("plan", &"[redacted authored event plan]") + .field("purpose", &self.purpose()) + .field("plan", &"[redacted exact event plan]") .field("authorization", &self.authorization) .field("policy", &self.policy) .finish_non_exhaustive() diff --git a/crates/signing/tests/authored_signing.rs b/crates/signing/tests/authored_signing.rs @@ -1,4 +1,8 @@ use nostr::{EventBuilder, JsonUtil, Keys, Kind as NostrKind, SecretKey, Timestamp}; +use radroots_blossom::{ + Sha256 as BlossomSha256, + authorization::{AuthoredUploadClaim, AuthorizationContent, ServerDomain}, +}; use radroots_event::{ GenericEventDraft, contract::AuthorRole, @@ -8,12 +12,12 @@ use radroots_event::{ }, wire::Nip01EventWire, }; -use radroots_event_codec::authoring::AuthoredEventPlan; +use radroots_event_codec::authoring::{AuthoredEventPlan, BlossomAuthorizationPlan}; use radroots_identity::{AccountId, PublicKey}; use radroots_protocol::runtime::v1::OperationId; use radroots_signing::{ Actor, AuthoredArtifactId, CurrentAuthoringAuthority, CurrentAuthoringDecision, Error, - SignReceipt, SignRequest, SigningIntentId, SigningOperationId, + SignReceipt, SignRequest, SigningIntentId, SigningOperationId, SigningPurpose, actor::ActorSource, authorization::ManagedSigningPolicy, error::Kind, @@ -84,6 +88,29 @@ fn request() -> SignRequest { .expect("request") } +fn blossom_plan() -> BlossomAuthorizationPlan { + let claim = AuthoredUploadClaim::new( + AuthorizationContent::parse("Upload exact image").expect("content"), + ServerDomain::parse("media.example").expect("server"), + BlossomSha256::digest(b"exact-image"), + CREATED_AT, + 60, + ) + .expect("upload claim"); + BlossomAuthorizationPlan::for_upload(&claim, public_key()).expect("authorization plan") +} + +fn blossom_request() -> SignRequest { + SignRequest::blossom_upload( + OperationId::SyncPush, + intent(9, 10), + actor(ActorSource::ExplicitPublicKey, [AuthorRole::Any]), + blossom_plan(), + policy(), + ) + .expect("Blossom request") +} + fn signed_event() -> radroots_event::SignedEvent { signed_event_with( &keys(), @@ -158,7 +185,7 @@ fn authorization_decisions_are_current_and_explicit() { .expect("explicitly allowed deprecated plan"); assert!(matches!( allowed.authorization(), - CurrentAuthoringDecision::AllowedDeprecated { .. } + Some(CurrentAuthoringDecision::AllowedDeprecated { .. }) )); } @@ -236,13 +263,127 @@ fn authorization_enforces_key_role_and_host_provenance() { } #[test] +fn blossom_request_is_http_only_domain_separated_and_author_bound() { + let request = blossom_request(); + assert_eq!( + request.purpose(), + SigningPurpose::BlossomUploadAuthorization + ); + assert!(request.authored_plan().is_none()); + assert_eq!(request.blossom_authorization_plan(), Some(&blossom_plan())); + assert_eq!(request.authorization(), None); + assert_eq!(request.kind(), 24_242); + + let wrong_key = + PublicKey::from_hex("e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af") + .expect("other public key"); + let wrong_actor = + Actor::new(wrong_key, ActorSource::ExplicitPublicKey, [AuthorRole::Any]).expect("actor"); + assert_eq!( + SignRequest::blossom_upload( + OperationId::SyncPush, + intent(9, 10), + wrong_actor, + blossom_plan(), + policy(), + ) + .expect_err("author mismatch") + .kind(), + Kind::AuthorizationDenied + ); +} + +#[test] +fn blossom_receipt_rejects_mismatch_lateness_and_cancellation() { + let request = blossom_request(); + let tags = request + .tags() + .iter() + .cloned() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("BUD-11 tags"); + let valid = signed_event_with( + &keys(), + 24_242, + request.content(), + request.created_at(), + tags.clone(), + ); + SignReceipt::from_signed_event(&request, valid, DEADLINE_MS - 1) + .expect("verified BUD-11 receipt"); + + let mismatched = signed_event_with( + &keys(), + 24_242, + "Upload a different image", + request.created_at(), + tags, + ); + assert_eq!( + SignReceipt::from_signed_event(&request, mismatched, DEADLINE_MS - 1) + .expect_err("mismatched output") + .kind(), + Kind::SignerOutputInvalid + ); + assert_eq!( + SignReceipt::from_signed_event( + &request, + signed_event_with( + &keys(), + 24_242, + request.content(), + request.created_at(), + request + .tags() + .iter() + .cloned() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("BUD-11 tags"), + ), + DEADLINE_MS, + ) + .expect_err("late output") + .kind(), + Kind::DeadlineExceeded + ); + + let signal = CancellationSignal::new(); + let cancelled = request.with_cancellation_signal(signal.clone()); + signal.cancel(); + assert_eq!( + SignReceipt::from_signed_event( + &cancelled, + signed_event_with( + &keys(), + 24_242, + cancelled.content(), + cancelled.created_at(), + cancelled + .tags() + .iter() + .cloned() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("BUD-11 tags"), + ), + DEADLINE_MS - 1, + ) + .expect_err("cancelled output") + .kind(), + Kind::SignerCancelled + ); +} + +#[test] fn request_identity_deadline_and_cancellation_are_exact() { let request = request(); let replay = request.clone(); assert_eq!(request.operation_kind(), OperationId::SyncPush); assert_eq!(request.intent_id(), intent(1, 2)); assert_eq!(request.actor().public_key(), public_key()); - assert_eq!(request.plan().digest(), plan().digest()); + assert_eq!(request.plan_digest(), plan().digest()); assert_eq!(request.policy(), policy()); assert!(!request.cancellation_signal().is_cancelled()); assert_eq!(request.signer_request_id(), replay.signer_request_id()); diff --git a/crates/signing/tests/package_boundary.rs b/crates/signing/tests/package_boundary.rs @@ -48,7 +48,7 @@ fn manifest_has_final_identity_features_and_dependencies() { ); assert_eq!( table_keys(MANIFEST, "[dev-dependencies]"), - BTreeSet::from(["nostr", "serde_json"]) + BTreeSet::from(["nostr", "radroots_blossom", "serde_json"]) ); for forbidden in [ "async-trait", @@ -115,7 +115,7 @@ fn crate_root_declares_the_approved_module_skeleton() { "pub use error::Error;", "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};", "pub use receipt::SignReceipt;", - "pub use request::SignRequest;", + "pub use request::{SignRequest, SigningPurpose};", "pub use signer::Signer;", "pub use status::SignerStatus;", ] { @@ -132,7 +132,7 @@ fn crate_root_declares_the_approved_module_skeleton() { "pub use error::Error;", "pub use identity::{AuthoredArtifactId, SignerRequestId, SigningIntentId, SigningOperationId};", "pub use receipt::SignReceipt;", - "pub use request::SignRequest;", + "pub use request::{SignRequest, SigningPurpose};", "pub use signer::Signer;", "pub use status::SignerStatus;", ]) @@ -172,6 +172,8 @@ fn package_documentation_and_reviewed_api_baseline_are_complete() { "pub mod radroots_signing::request", "pub mod radroots_signing::signer", "pub mod radroots_signing::status", + "pub enum radroots_signing::request::SigningPurpose", + "pub radroots_signing::request::SigningPurpose::BlossomUploadAuthorization", "pub trait radroots_signing::Signer", ] { assert!( diff --git a/crates/sync/src/push.rs b/crates/sync/src/push.rs @@ -8,7 +8,8 @@ use radroots_event_codec::{ }; use radroots_protocol::runtime::v1::OperationId; use radroots_signing::{ - Actor, AuthoredArtifactId as SigningArtifactId, SigningIntentId, SigningOperationId, + Actor, AuthoredArtifactId as SigningArtifactId, SignReceipt, SigningIntentId, + SigningOperationId, recovery::{RecoveryDisposition, ReplayCapability, recovery_disposition}, request::{CancellationPolicy, SignPolicy}, }; @@ -524,7 +525,22 @@ impl Engine { ) .map_err(|_| Error::InvalidPushRequest)?; - match signer.sign(sign_request).await { + let expected_request = sign_request.clone(); + let signer_result = match signer.sign(sign_request).await { + Ok(receipt) => match self.clock.now_unix_ms() { + Ok(observed_at_unix_ms) => SignReceipt::from_signed_event( + &expected_request, + receipt.signed_event().clone(), + observed_at_unix_ms, + ), + Err(_) => Err(radroots_signing::Error::new( + radroots_signing::error::Kind::InternalError, + )), + }, + Err(error) => Err(error), + }; + + match signer_result { Ok(receipt) => { let command = AuthoredAtomicCommand::ApplySigned( ApplySignedArtifact::new( @@ -550,6 +566,17 @@ impl Engine { } Err(error) => { let applied_at = self.clock.now_unix_ms()?; + if error.kind() == radroots_signing::error::Kind::DeadlineExceeded + && claimed + .signing_claim() + .is_some_and(|claim| applied_at >= claim.expires_at_unix_ms()) + { + // The signer result arrived after this worker's fence + // expired. It is rejected, but this stale worker must not + // mutate durable state; recovery will reclaim the exact + // request under a fresh fence. + return Err(Error::SignerDeadlineExceeded); + } if error.kind() == radroots_signing::error::Kind::SignerCancelled { let command = AuthoredAtomicCommand::Cancel( CancelAuthoredWork::new( diff --git a/crates/sync/tests/push_enqueue.rs b/crates/sync/tests/push_enqueue.rs @@ -798,6 +798,55 @@ impl Signer for MockSigner { } } +#[derive(Clone, Copy)] +enum BoundaryViolation { + CompletesAfterDeadline, + CancelsBeforeReturn, +} + +struct BoundaryViolatingSigner { + violation: BoundaryViolation, + clock: Arc<TestClock>, +} + +impl Signer for BoundaryViolatingSigner { + fn status( + &self, + ) -> radroots_signing::signer::BoxFuture<'_, Result<SignerStatus, SigningError>> { + Box::pin(async { + Ok(SignerStatus::new( + SignerAvailability::Ready, + vec![SignerCapability::new( + SignerKind::Remote, + ReplayCapability::ExactReplayByRequestId, + CancellationSupport::BeforeAndAfterPublication, + false, + false, + )], + None, + )) + }) + } + + fn sign( + &self, + request: SignRequest, + ) -> radroots_signing::signer::BoxFuture<'_, Result<SignReceipt, SigningError>> { + Box::pin(async move { + let deadline = request.policy().deadline_unix_ms(); + let receipt = + SignReceipt::from_signed_event(&request, signed_event(&request), deadline - 1)?; + match self.violation { + BoundaryViolation::CompletesAfterDeadline => { + self.clock.0.store(deadline, Ordering::Release); + } + BoundaryViolation::CancelsBeforeReturn => request.cancellation_signal().cancel(), + } + Ok(receipt) + }) + } +} + fn signing_keypair() -> Keypair { let secret = SecretKey::from_slice(&[1; 32]).expect("secret key"); Keypair::from_secret_key(&Secp256k1::new(), &secret) @@ -808,29 +857,28 @@ fn public_key_hex() -> String { } fn signed_event(request: &SignRequest) -> SignedEvent { - let plan = request.plan(); - let id = plan.expected_event_id().to_hex(); + let id = request.expected_event_id().to_hex(); let pubkey = public_key_hex(); let signature = Secp256k1::new() .sign_schnorr_no_aux_rand( - &Message::from_digest(*plan.expected_event_id().as_bytes()), + &Message::from_digest(*request.expected_event_id().as_bytes()), &signing_keypair(), ) .to_string(); let raw_json = format!( "{{\"id\":\"{id}\",\"pubkey\":\"{pubkey}\",\"created_at\":{},\"kind\":{},\"tags\":{:?},\"content\":{content:?},\"sig\":\"{signature}\"}}", - plan.created_at(), - plan.body().kind(), - plan.body().tags(), - content = plan.body().content(), + request.created_at(), + request.kind(), + request.tags(), + content = request.content(), ); SignedEvent::new(SignedEventParts { id, pubkey, - created_at: plan.created_at(), - kind: plan.body().kind(), - tags: plan.body().tags().to_vec(), - content: plan.body().content().to_owned(), + created_at: request.created_at(), + kind: request.kind(), + tags: request.tags().to_vec(), + content: request.content().to_owned(), sig: signature, raw_json, }) @@ -955,6 +1003,50 @@ fn execute_to_admitted(engine: &Engine, push: &PushRequest) { } #[test] +fn caller_revalidates_late_and_cancelled_signer_success_before_persistence() { + for (byte, violation, expected) in [ + ( + 55, + BoundaryViolation::CompletesAfterDeadline, + Error::SignerDeadlineExceeded, + ), + ( + 56, + BoundaryViolation::CancelsBeforeReturn, + Error::SigningCancelled, + ), + ] { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([byte; 32]).expect("generation"), + )); + let capability: Arc<dyn SyncStorage> = storage; + let clock = Arc::new(TestClock(AtomicU64::new(1_800_000_200_000))); + let signer: Arc<dyn Signer> = Arc::new(BoundaryViolatingSigner { + violation, + clock: Arc::clone(&clock), + }); + let engine = Engine::builder( + capability, + clock, + Arc::new(TestIds(AtomicU64::new(10))), + DeadlinePolicy::new(10_000, 10_000, 10_000).expect("deadlines"), + ) + .sink(Arc::new(MockSink)) + .signer(signer) + .build() + .expect("engine"); + let push = request(byte, "wss://relay.example"); + + assert_eq!(block_on(engine.sign_prepared(push.clone())), Err(expected)); + let status = block_on(engine.push_status(push.operation_id())) + .expect("status") + .expect("prepared operation"); + assert!(status.artifact().signed().is_none()); + assert!(status.delivery_plan().attempts().is_empty()); + } +} + +#[test] fn preparation_is_atomic_status_visible_and_replays_without_external_effects() { let signer = Arc::new(MockSigner::new(SignBehavior::Pending)); let (engine, storage) = setup_engine(signer.clone()); diff --git a/docs/api/README.md b/docs/api/README.md @@ -45,3 +45,5 @@ expand a package beyond its charter. | `radroots_storage` | [`radroots_storage.txt`](radroots_storage.txt) | [release V1 specification](../specs/radroots_crates_release_v1.md) | | `radroots_transport_nostr` | [`radroots_transport_nostr.txt`](radroots_transport_nostr.txt) | [release V1 specification](../specs/radroots_crates_release_v1.md) | | `radroots_geonames` | [`radroots_geonames.txt`](radroots_geonames.txt) | [release V1 specification](../specs/radroots_crates_release_v1.md) | +| `radroots_sdk` | [`radroots_sdk.txt`](radroots_sdk.txt) | [release V1 specification](../specs/radroots_crates_release_v1.md) | +| `radroots` | [`radroots.txt`](radroots.txt) | [release V1 specification](../specs/radroots_crates_release_v1.md) | diff --git a/docs/api/radroots.txt b/docs/api/radroots.txt @@ -0,0 +1,131 @@ +pub mod radroots +pub use radroots::Client +pub use radroots::ClientBuilder +pub use radroots::Error +pub use radroots::Result +pub mod radroots::client +pub const fn radroots::client::geonames_enabled() -> bool +pub const fn radroots::client::knowledge_enabled() -> bool +pub const fn radroots::client::local_only() -> radroots_sdk::transport::Profile +pub fn radroots::client::memory() -> radroots_sdk::client::ClientBuilder +pub async fn radroots::client::native(radroots_sdk::storage::SqliteOptions) -> radroots_sdk::error::Result<radroots_sdk::client::ClientBuilder> +pub fn radroots::client::with_nip46_signer(radroots_sdk::client::ClientBuilder, radroots_sdk::signing::Provider) -> radroots_sdk::client::ClientBuilder +pub fn radroots::client::with_transport(radroots_sdk::client::ClientBuilder, alloc::sync::Arc<dyn radroots_transport::source::EventSource>, alloc::sync::Arc<dyn radroots_transport::sink::EventSink>) -> radroots_sdk::client::ClientBuilder +pub mod radroots::event +pub use radroots::event::Error +pub use radroots::event::Event +pub use radroots::event::EventId +pub use radroots::event::EventKind +pub use radroots::event::EventTag +pub use radroots::event::GenericEventDraft +pub use radroots::event::SignedEvent +pub use radroots::event::VerifiedEvent +pub mod radroots::farm +pub use radroots::farm::Farm +pub use radroots::farm::FarmPublicLocation +pub use radroots::farm::FarmRef +pub use radroots::farm::Plan +pub use radroots::farm::PrepareError +pub use radroots::farm::PrepareErrorKind +pub use radroots::farm::PrepareRequest +pub use radroots::farm::prepare +pub mod radroots::identity +pub use radroots::identity::AccountId +pub use radroots::identity::Error +pub use radroots::identity::IdentityId +pub use radroots::identity::Profile +pub use radroots::identity::PublicIdentity +pub use radroots::identity::PublicKey +pub use radroots::identity::Username +pub mod radroots::knowledge +pub use radroots::knowledge::AddressableRef +pub use radroots::knowledge::KnowledgeClaim +pub use radroots::knowledge::KnowledgeFieldReport +pub use radroots::knowledge::KnowledgeRelation +pub use radroots::knowledge::KnowledgeReview +pub use radroots::knowledge::KnowledgeSource +pub use radroots::knowledge::KnowledgeValidationError +pub use radroots::knowledge::WikiArticle +pub use radroots::knowledge::WikiDTagError +pub use radroots::knowledge::WikiMergeRequest +pub use radroots::knowledge::WikiRedirect +pub use radroots::knowledge::normalize_wiki_d_tag +pub use radroots::knowledge::validate_wiki_d_tag +pub mod radroots::listing +pub use radroots::listing::Action +pub use radroots::listing::EditV1 +pub use radroots::listing::Lifecycle +pub use radroots::listing::OperationalListing +pub use radroots::listing::OperationalListingAvailability +pub use radroots::listing::OperationalListingBin +pub use radroots::listing::OperationalListingDeliveryMethod +pub use radroots::listing::OperationalListingImage +pub use radroots::listing::OperationalListingProduct +pub use radroots::listing::OperationalListingPublicLocation +pub use radroots::listing::OperationalListingStatus +pub use radroots::listing::Plan +pub use radroots::listing::PrepareError +pub use radroots::listing::PrepareErrorKind +pub use radroots::listing::PrepareRequest +pub use radroots::listing::prepare +pub mod radroots::signing +pub use radroots::signing::AuthorizationHeader +pub use radroots::signing::BlossomAuthorizationPlan +pub use radroots::signing::BlossomSigningError +pub use radroots::signing::Mode +pub use radroots::signing::Operations +pub use radroots::signing::Provider +pub use radroots::signing::blossom_upload_request +pub mod radroots::storage +pub use radroots::storage::IntegrityStatus +pub use radroots::storage::Operations +pub use radroots::storage::SqliteOpenMode +pub use radroots::storage::SqliteOptions +pub use radroots::storage::SqlitePaths +pub use radroots::storage::Status +pub mod radroots::sync +pub mod radroots::trade +pub use radroots::trade::Currency +pub use radroots::trade::Decimal +pub use radroots::trade::FulfillmentProfileV1 +pub use radroots::trade::Money +pub use radroots::trade::MutationBodyV1 +pub use radroots::trade::MutationKindV1 +pub use radroots::trade::MutationV1 +pub use radroots::trade::Percent +pub use radroots::trade::Plan +pub use radroots::trade::PrepareError +pub use radroots::trade::PrepareErrorKind +pub use radroots::trade::PrepareRequest +pub use radroots::trade::Projection +pub use radroots::trade::Quantity +pub use radroots::trade::QuantityPrice +pub use radroots::trade::ReducerIssue +pub use radroots::trade::ReductionInput +pub use radroots::trade::TradeCancellationProfileV1 +pub use radroots::trade::TradeCandidateLineV1 +pub use radroots::trade::TradeCandidateTermsV1 +pub use radroots::trade::TradeDecisionV1 +pub use radroots::trade::TradeEconomicAdjustmentV1 +pub use radroots::trade::TradeEconomicsProfileV1 +pub use radroots::trade::TradePrivateTermsRefV1 +pub use radroots::trade::Unit +pub use radroots::trade::ValidationError +pub use radroots::trade::WorkflowPlan +pub use radroots::trade::prepare +pub use radroots::trade::project +pub mod radroots::transport +pub use radroots::transport::DaemonAuth +pub use radroots::transport::DaemonConfig +pub use radroots::transport::DaemonDelivery +pub use radroots::transport::DaemonError +pub use radroots::transport::Error +pub use radroots::transport::EventSink +pub use radroots::transport::EventSource +pub use radroots::transport::Profile +pub use radroots::transport::SatisfactionClass +pub use radroots::transport::SatisfactionPolicy +pub use radroots::transport::Target +pub use radroots::transport::TargetPolicy +pub use radroots::transport::TargetSet +pub use radroots::transport::TransportId diff --git a/docs/api/radroots_event_codec.txt b/docs/api/radroots_event_codec.txt @@ -165,6 +165,141 @@ pub fn radroots_event_codec::admission::RadrootsEventAdmissionError::source(&sel impl core::fmt::Display for radroots_event_codec::admission::RadrootsEventAdmissionError pub fn radroots_event_codec::admission::RadrootsEventAdmissionError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub fn radroots_event_codec::admission::admit_verified_event(crate::verification::RadrootsSignatureVerifiedEvent) -> core::result::Result<radroots_event_codec::admission::RadrootsAdmittedEvent, radroots_event_codec::admission::RadrootsEventAdmissionError> +pub mod radroots_event_codec::authoring +#[non_exhaustive] pub enum radroots_event_codec::authoring::AuthoredPlanError +pub radroots_event_codec::authoring::AuthoredPlanError::Calendar(radroots_event_codec::encode::EventEncodeError) +pub radroots_event_codec::authoring::AuthoredPlanError::CanonicalEventId(radroots_event::wire::v1::CanonicalEventIdError) +pub radroots_event_codec::authoring::AuthoredPlanError::ContentTooLarge +pub radroots_event_codec::authoring::AuthoredPlanError::ContentTooLarge::actual: usize +pub radroots_event_codec::authoring::AuthoredPlanError::ContentTooLarge::max: usize +pub radroots_event_codec::authoring::AuthoredPlanError::ContractIdentity(radroots_event::contract::ContractIdentityError) +pub radroots_event_codec::authoring::AuthoredPlanError::ContractKindMismatch +pub radroots_event_codec::authoring::AuthoredPlanError::ContractKindMismatch::actual: u32 +pub radroots_event_codec::authoring::AuthoredPlanError::ContractKindMismatch::expected: u32 +pub radroots_event_codec::authoring::AuthoredPlanError::ContractNotTyped +pub radroots_event_codec::authoring::AuthoredPlanError::ContractNotTyped::contract_id: alloc::string::String +pub radroots_event_codec::authoring::AuthoredPlanError::Envelope(radroots_event::envelope::EventEnvelopeError) +pub radroots_event_codec::authoring::AuthoredPlanError::FoodAvailability(radroots_event_codec::encode::food_availability::RadrootsFoodAvailabilityEncodeError) +pub radroots_event_codec::authoring::AuthoredPlanError::InvalidAuthor(radroots_identity::error::Error) +pub radroots_event_codec::authoring::AuthoredPlanError::Profile(radroots_event_codec::encode::profile::RadrootsAuthoredProfileEncodeError) +impl radroots_event_codec::authoring::AuthoredPlanError +pub const fn radroots_event_codec::authoring::AuthoredPlanError::code(&self) -> &'static str +impl core::error::Error for radroots_event_codec::authoring::AuthoredPlanError +impl core::fmt::Display for radroots_event_codec::authoring::AuthoredPlanError +pub fn radroots_event_codec::authoring::AuthoredPlanError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum radroots_event_codec::authoring::PlanDecodeError +pub radroots_event_codec::authoring::PlanDecodeError::ContractIdentity(radroots_event::contract::ContractIdentityError) +pub radroots_event_codec::authoring::PlanDecodeError::ContractKindMismatch +pub radroots_event_codec::authoring::PlanDecodeError::ContractKindMismatch::actual: u32 +pub radroots_event_codec::authoring::PlanDecodeError::ContractKindMismatch::expected: u32 +pub radroots_event_codec::authoring::PlanDecodeError::EventIdMismatch +pub radroots_event_codec::authoring::PlanDecodeError::EventIdMismatch::computed: alloc::string::String +pub radroots_event_codec::authoring::PlanDecodeError::EventIdMismatch::declared: alloc::string::String +pub radroots_event_codec::authoring::PlanDecodeError::ExpectedAuthor(alloc::string::String) +pub radroots_event_codec::authoring::PlanDecodeError::ExpectedEventId(alloc::string::String) +pub radroots_event_codec::authoring::PlanDecodeError::HistoricalProfileUnavailable +pub radroots_event_codec::authoring::PlanDecodeError::HistoricalProfileUnavailable::contract_id: alloc::string::String +pub radroots_event_codec::authoring::PlanDecodeError::HistoricalShape(alloc::string::String) +pub radroots_event_codec::authoring::PlanDecodeError::Json(alloc::string::String) +pub radroots_event_codec::authoring::PlanDecodeError::NonCanonicalExpectedAuthor +pub radroots_event_codec::authoring::PlanDecodeError::NonCanonicalExpectedEventId +pub radroots_event_codec::authoring::PlanDecodeError::PlanDigest(radroots_event_codec::authoring::PlanDigestError) +pub radroots_event_codec::authoring::PlanDecodeError::PlanDigestMismatch +pub radroots_event_codec::authoring::PlanDecodeError::PlanDigestMismatch::computed: alloc::string::String +pub radroots_event_codec::authoring::PlanDecodeError::PlanDigestMismatch::declared: alloc::string::String +pub radroots_event_codec::authoring::PlanDecodeError::RawJsonTooLarge +pub radroots_event_codec::authoring::PlanDecodeError::RawJsonTooLarge::actual: usize +pub radroots_event_codec::authoring::PlanDecodeError::RawJsonTooLarge::max: usize +pub radroots_event_codec::authoring::PlanDecodeError::UnsupportedSchemaVersion +pub radroots_event_codec::authoring::PlanDecodeError::UnsupportedSchemaVersion::actual: u32 +impl core::error::Error for radroots_event_codec::authoring::PlanDecodeError +impl core::fmt::Display for radroots_event_codec::authoring::PlanDecodeError +pub fn radroots_event_codec::authoring::PlanDecodeError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum radroots_event_codec::authoring::PlanRegistryRelation +pub radroots_event_codec::authoring::PlanRegistryRelation::Current +pub radroots_event_codec::authoring::PlanRegistryRelation::Historical +pub struct radroots_event_codec::authoring::AuthoredEventBody +impl radroots_event_codec::authoring::AuthoredEventBody +pub fn radroots_event_codec::authoring::AuthoredEventBody::content(&self) -> &str +pub const fn radroots_event_codec::authoring::AuthoredEventBody::contract(&self) -> &radroots_event::contract::ContractKey +pub const fn radroots_event_codec::authoring::AuthoredEventBody::kind(&self) -> u32 +pub fn radroots_event_codec::authoring::AuthoredEventBody::tags(&self) -> &[alloc::vec::Vec<alloc::string::String>] +impl radroots_event_codec::authoring::AuthoredEventBody +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_ask(&radroots_event::post::AuthoredAsk) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_calendar_date_event(&radroots_event::calendar::AuthoredCalendarDateEvent) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_calendar_time_event(&radroots_event::calendar::AuthoredCalendarTimeEvent) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_food_availability(&radroots_event::food::availability::FoodAvailabilityDetails, u64) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_nip09_deletion_request(&radroots_event::post::deletion::AuthoredNip09DeletionRequest) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_nip10_reply(&radroots_event::post::reply::AuthoredNip10Reply) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_nip22_comment(&radroots_event::post::comment::AuthoredNip22Comment) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_photo_update(&radroots_event::post::AuthoredPhotoUpdate) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_profile(&radroots_event::profile::AuthoredProfile) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventBody::from_update(&radroots_event::post::AuthoredUpdate) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub struct radroots_event_codec::authoring::AuthoredEventPlan +impl radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_event_codec::authoring::AuthoredEventPlan::author(&self) -> &radroots_identity::key::PublicKey +pub const fn radroots_event_codec::authoring::AuthoredEventPlan::body(&self) -> &radroots_event_codec::authoring::AuthoredEventBody +pub const fn radroots_event_codec::authoring::AuthoredEventPlan::created_at(&self) -> u64 +pub const fn radroots_event_codec::authoring::AuthoredEventPlan::digest(&self) -> radroots_event_codec::authoring::PlanDigest +pub const fn radroots_event_codec::authoring::AuthoredEventPlan::expected_event_id(&self) -> &radroots_event::id::EventId +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_generic(radroots_event::generic_draft::GenericEventDraft) -> core::result::Result<Self, radroots_event::draft::DraftError> +impl radroots_event_codec::authoring::AuthoredEventPlan +pub fn radroots_event_codec::authoring::AuthoredEventPlan::bind(radroots_event_codec::authoring::AuthoredEventBody, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_ask(&radroots_event::post::AuthoredAsk, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_calendar_date_event(&radroots_event::calendar::AuthoredCalendarDateEvent, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_calendar_time_event(&radroots_event::calendar::AuthoredCalendarTimeEvent, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_food_availability(&radroots_event::food::availability::FoodAvailabilityDetails, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_nip09_deletion_request(&radroots_event::post::deletion::AuthoredNip09DeletionRequest, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_nip10_reply(&radroots_event::post::reply::AuthoredNip10Reply, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_nip22_comment(&radroots_event::post::comment::AuthoredNip22Comment, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_photo_update(&radroots_event::post::AuthoredPhotoUpdate, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_profile(&radroots_event::profile::AuthoredProfile, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub fn radroots_event_codec::authoring::AuthoredEventPlan::from_update(&radroots_event::post::AuthoredUpdate, u64, impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event_codec::authoring::AuthoredPlanError> +pub struct radroots_event_codec::authoring::BlossomAuthorizationPlan +impl radroots_event_codec::authoring::BlossomAuthorizationPlan +pub const fn radroots_event_codec::authoring::BlossomAuthorizationPlan::author(&self) -> &radroots_identity::key::PublicKey +pub fn radroots_event_codec::authoring::BlossomAuthorizationPlan::content(&self) -> &str +pub const fn radroots_event_codec::authoring::BlossomAuthorizationPlan::created_at(&self) -> u64 +pub const fn radroots_event_codec::authoring::BlossomAuthorizationPlan::digest(&self) -> radroots_event_codec::authoring::PlanDigest +pub const fn radroots_event_codec::authoring::BlossomAuthorizationPlan::expected_event_id(&self) -> &radroots_event::id::EventId +pub fn radroots_event_codec::authoring::BlossomAuthorizationPlan::for_upload(&radroots_blossom::authorization::AuthoredUploadClaim, radroots_identity::key::PublicKey) -> core::result::Result<Self, radroots_event::wire::v1::CanonicalEventIdError> +pub const fn radroots_event_codec::authoring::BlossomAuthorizationPlan::kind(&self) -> u32 +pub fn radroots_event_codec::authoring::BlossomAuthorizationPlan::tags(&self) -> &[alloc::vec::Vec<alloc::string::String>] +pub struct radroots_event_codec::authoring::HistoricalPlanIntegrity +impl radroots_event_codec::authoring::HistoricalPlanIntegrity +pub fn radroots_event_codec::authoring::HistoricalPlanIntegrity::into_plan(self) -> radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_event_codec::authoring::HistoricalPlanIntegrity::plan(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub fn radroots_event_codec::authoring::HistoricalPlanIntegrity::registry_relation(&self, radroots_event::contract::RegistryVersion) -> radroots_event_codec::authoring::PlanRegistryRelation +pub struct radroots_event_codec::authoring::PlanDigest(_) +impl radroots_event_codec::authoring::PlanDigest +pub const radroots_event_codec::authoring::PlanDigest::BYTE_LENGTH: usize +pub const fn radroots_event_codec::authoring::PlanDigest::as_bytes(&self) -> &[u8; 32] +pub const fn radroots_event_codec::authoring::PlanDigest::from_bytes([u8; 32]) -> Self +pub fn radroots_event_codec::authoring::PlanDigest::parse_hex(&str) -> core::result::Result<Self, radroots_event_codec::authoring::PlanDigestError> +pub fn radroots_event_codec::authoring::PlanDigest::to_hex(self) -> alloc::string::String +pub struct radroots_event_codec::authoring::PlanDigestError +impl core::error::Error for radroots_event_codec::authoring::PlanDigestError +impl core::fmt::Display for radroots_event_codec::authoring::PlanDigestError +pub fn radroots_event_codec::authoring::PlanDigestError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_event_codec::authoring::PlanWireV1 +impl radroots_event_codec::authoring::PlanWireV1 +pub fn radroots_event_codec::authoring::PlanWireV1::content(&self) -> &str +pub const fn radroots_event_codec::authoring::PlanWireV1::contract(&self) -> &radroots_event::contract::ContractKey +pub const fn radroots_event_codec::authoring::PlanWireV1::created_at(&self) -> u64 +pub const fn radroots_event_codec::authoring::PlanWireV1::expected_author(&self) -> &radroots_identity::key::PublicKey +pub const fn radroots_event_codec::authoring::PlanWireV1::expected_event_id(&self) -> &radroots_event::id::EventId +pub fn radroots_event_codec::authoring::PlanWireV1::from_json(&[u8]) -> core::result::Result<radroots_event_codec::authoring::HistoricalPlanIntegrity, radroots_event_codec::authoring::PlanDecodeError> +pub fn radroots_event_codec::authoring::PlanWireV1::from_plan(&radroots_event_codec::authoring::AuthoredEventPlan) -> Self +pub const fn radroots_event_codec::authoring::PlanWireV1::kind(&self) -> u32 +pub const fn radroots_event_codec::authoring::PlanWireV1::plan_digest(&self) -> radroots_event_codec::authoring::PlanDigest +pub const fn radroots_event_codec::authoring::PlanWireV1::schema_version(&self) -> u32 +pub fn radroots_event_codec::authoring::PlanWireV1::tags(&self) -> &[alloc::vec::Vec<alloc::string::String>] +pub fn radroots_event_codec::authoring::PlanWireV1::to_json(&self) -> core::result::Result<alloc::vec::Vec<u8>, radroots_event_codec::authoring::PlanDecodeError> +impl serde_core::ser::Serialize for radroots_event_codec::authoring::PlanWireV1 +pub fn radroots_event_codec::authoring::PlanWireV1::serialize<S>(&self, S) -> core::result::Result<<S as serde_core::ser::Serializer>::Ok, <S as serde_core::ser::Serializer>::Error> where S: serde_core::ser::Serializer +pub const radroots_event_codec::authoring::PLAN_WIRE_MAX_BYTES: usize +pub const radroots_event_codec::authoring::PLAN_WIRE_VERSION_V1: u32 +pub const radroots_event_codec::authoring::REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS: [&str; 10] pub mod radroots_event_codec::canonical pub fn radroots_event_codec::canonical::id(&radroots_event::envelope::EventEnvelope) -> core::result::Result<radroots_event::id::EventId, radroots_event_codec::canonical::CanonicalError> pub fn radroots_event_codec::canonical::id_preimage(&radroots_event::envelope::EventEnvelope) -> core::result::Result<alloc::string::String, radroots_event_codec::canonical::CanonicalError> @@ -446,36 +581,7 @@ pub fn radroots_event_codec::decode::plot::data_from_event(alloc::string::String pub fn radroots_event_codec::decode::plot::parsed_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>, alloc::string::String) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedEvent<radroots_event::farm::plot::Plot>, radroots_event_codec::decode::EventParseError> pub fn radroots_event_codec::decode::plot::plot_from_event(u32, &[alloc::vec::Vec<alloc::string::String>], &str) -> core::result::Result<radroots_event::farm::plot::Plot, radroots_event_codec::decode::EventParseError> pub mod radroots_event_codec::decode::post -pub struct radroots_event_codec::decode::post::LegacyPost -pub radroots_event_codec::decode::post::LegacyPost::address_refs: core::option::Option<alloc::vec::Vec<radroots_event::social::SocialTarget>> -pub radroots_event_codec::decode::post::LegacyPost::content: alloc::string::String -pub radroots_event_codec::decode::post::LegacyPost::farm: core::option::Option<radroots_event::social::SocialFarmAnchor> -pub radroots_event_codec::decode::post::LegacyPost::location: core::option::Option<radroots_event::social::SocialLocation> -pub radroots_event_codec::decode::post::LegacyPost::media: core::option::Option<alloc::vec::Vec<radroots_event::social::SocialMediaMetadata>> -pub radroots_event_codec::decode::post::LegacyPost::quote_refs: core::option::Option<alloc::vec::Vec<radroots_event::social::SocialTarget>> -pub radroots_event_codec::decode::post::LegacyPost::topics: core::option::Option<alloc::vec::Vec<alloc::string::String>> -pub fn radroots_event_codec::decode::post::data_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedData<radroots_event_codec::decode::post::LegacyPost>, radroots_event_codec::decode::EventParseError> -pub fn radroots_event_codec::decode::post::parsed_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>, alloc::string::String) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedEvent<radroots_event_codec::decode::post::LegacyPost>, radroots_event_codec::decode::EventParseError> -pub fn radroots_event_codec::decode::post::post_from_content(u32, &str) -> core::result::Result<radroots_event_codec::decode::post::LegacyPost, radroots_event_codec::decode::EventParseError> -pub fn radroots_event_codec::decode::post::post_from_event(u32, &[alloc::vec::Vec<alloc::string::String>], &str) -> core::result::Result<radroots_event_codec::decode::post::LegacyPost, radroots_event_codec::decode::EventParseError> pub mod radroots_event_codec::decode::profile -pub struct radroots_event_codec::decode::profile::LegacyProfile -pub radroots_event_codec::decode::profile::LegacyProfile::about: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::banner: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::bot: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::display_name: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::lud06: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::lud16: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::name: alloc::string::String -pub radroots_event_codec::decode::profile::LegacyProfile::nip05: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::picture: core::option::Option<alloc::string::String> -pub radroots_event_codec::decode::profile::LegacyProfile::website: core::option::Option<alloc::string::String> -pub struct radroots_event_codec::decode::profile::RadrootsProfileData -pub radroots_event_codec::decode::profile::RadrootsProfileData::profile: radroots_event_codec::decode::profile::LegacyProfile -pub radroots_event_codec::decode::profile::RadrootsProfileData::profile_type: core::option::Option<radroots_event::profile::ProfileType> -pub fn radroots_event_codec::decode::profile::data_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedData<radroots_event_codec::decode::profile::RadrootsProfileData>, radroots_event_codec::decode::EventParseError> -pub fn radroots_event_codec::decode::profile::parsed_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>, alloc::string::String) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedEvent<radroots_event_codec::decode::profile::RadrootsProfileData>, radroots_event_codec::decode::EventParseError> -pub fn radroots_event_codec::decode::profile::profile_from_content(&str) -> core::result::Result<radroots_event_codec::decode::profile::LegacyProfile, radroots_event_codec::decode::EventParseError> pub mod radroots_event_codec::decode::reaction pub fn radroots_event_codec::decode::reaction::data_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedData<radroots_event::post::reaction::Reaction>, radroots_event_codec::decode::EventParseError> pub fn radroots_event_codec::decode::reaction::parsed_from_event(alloc::string::String, alloc::string::String, u64, u32, alloc::string::String, alloc::vec::Vec<alloc::vec::Vec<alloc::string::String>>, alloc::string::String) -> core::result::Result<radroots_event_codec::decode::parsed::RadrootsParsedEvent<radroots_event::post::reaction::Reaction>, radroots_event_codec::decode::EventParseError> diff --git a/docs/api/radroots_nostr.txt b/docs/api/radroots_nostr.txt @@ -36,6 +36,7 @@ impl radroots_nostr::blossom::SignedAuthorization pub fn radroots_nostr::blossom::SignedAuthorization::author(&self) -> nostr::key::public_key::PublicKey pub fn radroots_nostr::blossom::SignedAuthorization::created_at(&self) -> radroots_nostr::event::Timestamp pub fn radroots_nostr::blossom::SignedAuthorization::event_id(&self) -> radroots_nostr::event::EventId +pub fn radroots_nostr::blossom::SignedAuthorization::from_signed_event(&radroots_event::draft::SignedEvent) -> core::result::Result<Self, radroots_nostr::blossom::AuthorizationError> impl core::fmt::Debug for radroots_nostr::blossom::SignedAuthorization pub fn radroots_nostr::blossom::SignedAuthorization::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_nostr::blossom::VerifiedAuthorization @@ -48,6 +49,7 @@ impl core::fmt::Debug for radroots_nostr::blossom::VerifiedAuthorization pub fn radroots_nostr::blossom::VerifiedAuthorization::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub fn radroots_nostr::blossom::decode_verify_authorization_header(&str, &radroots_blossom::authorization::AuthorizationValidation) -> core::result::Result<radroots_nostr::blossom::VerifiedAuthorization, radroots_nostr::blossom::AuthorizationError> pub fn radroots_nostr::blossom::encode_authorization_header(&radroots_nostr::blossom::SignedAuthorization) -> radroots_nostr::blossom::AuthorizationHeader +pub fn radroots_nostr::blossom::encode_signed_event_authorization_header(&radroots_event::draft::SignedEvent) -> core::result::Result<radroots_nostr::blossom::AuthorizationHeader, radroots_nostr::blossom::AuthorizationError> pub fn radroots_nostr::blossom::sign_authorization(&nostr::key::Keys, &radroots_blossom::authorization::AuthoredUploadClaim) -> core::result::Result<radroots_nostr::blossom::SignedAuthorization, radroots_nostr::blossom::AuthorizationError> pub mod radroots_nostr::event pub enum radroots_nostr::event::Verification @@ -70,6 +72,11 @@ pub fn radroots_nostr::event::ApplicationHandlerSpec::with_identifier(self, impl pub fn radroots_nostr::event::ApplicationHandlerSpec::with_metadata(self, radroots_nostr::event::Metadata) -> Self pub fn radroots_nostr::event::ApplicationHandlerSpec::with_nostr_connect_url(self, impl core::convert::Into<alloc::string::String>) -> Self pub fn radroots_nostr::event::ApplicationHandlerSpec::with_relays(self, alloc::vec::Vec<alloc::string::String>) -> Self +pub struct radroots_nostr::event::CalendarEventBuilder +impl radroots_nostr::event::CalendarEventBuilder +pub fn radroots_nostr::event::CalendarEventBuilder::custom_created_at(self, radroots_nostr::event::Timestamp) -> Self +pub fn radroots_nostr::event::CalendarEventBuilder::into_external_signing_request(self, nostr::key::public_key::PublicKey) -> core::result::Result<radroots_nostr::event::ExternalSigningRequest, radroots_nostr::Error> +pub fn radroots_nostr::event::CalendarEventBuilder::sign_with_keys(self, &nostr::key::Keys) -> core::result::Result<radroots_nostr::event::Event, radroots_nostr::Error> pub struct radroots_nostr::event::EventAdapter<'a> impl<'a> radroots_nostr::event::EventAdapter<'a> pub fn radroots_nostr::event::EventAdapter<'a>::new(&'a radroots_nostr::event::Event) -> Self @@ -141,6 +148,8 @@ impl radroots_event::verification::SignatureVerifier for radroots_nostr::event:: pub fn radroots_nostr::event::SignatureVerifier::verify_signature(&self, &radroots_event::envelope::EventEnvelope) -> core::result::Result<(), radroots_event::verification::Error> pub fn radroots_nostr::event::build_application_handler(&radroots_nostr::event::ApplicationHandlerSpec) -> core::result::Result<radroots_nostr::event::GenericBuilder, radroots_nostr::Error> pub fn radroots_nostr::event::build_ask(&radroots_event::post::AuthoredAsk) -> core::result::Result<radroots_nostr::event::PostBuilder, radroots_nostr::Error> +pub fn radroots_nostr::event::build_calendar_date(&radroots_event::calendar::AuthoredCalendarDateEvent) -> core::result::Result<radroots_nostr::event::CalendarEventBuilder, radroots_nostr::Error> +pub fn radroots_nostr::event::build_calendar_time(&radroots_event::calendar::AuthoredCalendarTimeEvent) -> core::result::Result<radroots_nostr::event::CalendarEventBuilder, radroots_nostr::Error> pub fn radroots_nostr::event::build_food_availability(&radroots_event::food::availability::FoodAvailabilityDetails, radroots_nostr::event::Timestamp) -> core::result::Result<radroots_nostr::event::FoodAvailabilityBuilder, radroots_nostr::Error> pub fn radroots_nostr::event::build_job_feedback(&radroots_nostr::event::Event, &str, core::option::Option<alloc::string::String>, core::option::Option<alloc::vec::Vec<radroots_nostr::tag::Tag>>) -> core::result::Result<radroots_nostr::event::GenericBuilder, radroots_nostr::Error> pub fn radroots_nostr::event::build_job_result(&radroots_nostr::event::Event, impl core::convert::Into<alloc::string::String>, u64, core::option::Option<alloc::string::String>, core::option::Option<alloc::vec::Vec<radroots_nostr::tag::Tag>>) -> core::result::Result<radroots_nostr::event::GenericBuilder, radroots_nostr::Error> @@ -165,8 +174,6 @@ pub fn radroots_nostr::event::to_job_request_metadata(&radroots_nostr::event::Ev pub fn radroots_nostr::event::to_job_result_index(&radroots_nostr::event::Event) -> core::result::Result<radroots_event_codec::parsed::RadrootsParsedEvent<radroots_event::social::job_result::JobResult>, radroots_event_codec::job::error::JobParseError> pub fn radroots_nostr::event::to_job_result_metadata(&radroots_nostr::event::Event) -> core::result::Result<radroots_event_codec::parsed::RadrootsParsedData<radroots_event::social::job_result::JobResult>, radroots_event_codec::job::error::JobParseError> pub fn radroots_nostr::event::to_nostr(&radroots_event::envelope::EventEnvelope) -> core::result::Result<radroots_nostr::event::Event, radroots_nostr::Error> -pub fn radroots_nostr::event::to_post_event_metadata(&radroots_nostr::event::Event) -> radroots_event_codec::parsed::RadrootsParsedData<radroots_event_codec::post::decode::LegacyPost> -pub fn radroots_nostr::event::to_profile_event_metadata(&radroots_nostr::event::Event) -> core::option::Option<radroots_event_codec::parsed::RadrootsParsedData<radroots_event_codec::profile::RadrootsProfileData>> pub fn radroots_nostr::event::verify(&radroots_event::envelope::EventEnvelope) -> radroots_nostr::event::Verification pub fn radroots_nostr::event::verify_id(&radroots_event::envelope::EventEnvelope) -> radroots_nostr::event::Verification pub type radroots_nostr::event::Coordinate = nostr::nips::nip01::Coordinate diff --git a/docs/api/radroots_sdk.txt b/docs/api/radroots_sdk.txt @@ -0,0 +1,518 @@ +pub mod radroots_sdk +pub mod radroots_sdk::capability +pub enum radroots_sdk::capability::Availability +pub radroots_sdk::capability::Availability::Available +pub radroots_sdk::capability::Availability::Degraded +pub radroots_sdk::capability::Availability::Unavailable +pub radroots_sdk::capability::Availability::Unsupported +pub enum radroots_sdk::capability::Maturity +pub radroots_sdk::capability::Maturity::Experimental +pub radroots_sdk::capability::Maturity::Preview +pub radroots_sdk::capability::Maturity::Stable +pub struct radroots_sdk::capability::CapabilityId(_) +impl radroots_sdk::capability::CapabilityId +pub const radroots_sdk::capability::CapabilityId::BACKUP_RESTORE: Self +pub const radroots_sdk::capability::CapabilityId::CANONICAL_STORAGE: Self +pub const radroots_sdk::capability::CapabilityId::DAEMON_DELIVERY: Self +pub const radroots_sdk::capability::CapabilityId::FARM_PUBLICATION: Self +pub const radroots_sdk::capability::CapabilityId::KNOWLEDGE_EVENTS: Self +pub const radroots_sdk::capability::CapabilityId::LISTING_PUBLICATION: Self +pub const radroots_sdk::capability::CapabilityId::LOCAL_SIGNING: Self +pub const radroots_sdk::capability::CapabilityId::MESH_TRANSPORT: Self +pub const radroots_sdk::capability::CapabilityId::NIP46_SIGNING: Self +pub const radroots_sdk::capability::CapabilityId::NOSTR_DELIVERY: Self +pub const radroots_sdk::capability::CapabilityId::NOSTR_FETCH: Self +pub const radroots_sdk::capability::CapabilityId::PERSISTENT_STORAGE: Self +pub const radroots_sdk::capability::CapabilityId::RETICULUM_DELIVERY: Self +pub const radroots_sdk::capability::CapabilityId::RETICULUM_FETCH: Self +pub const radroots_sdk::capability::CapabilityId::SIMPLEX_TRANSPORT: Self +pub const radroots_sdk::capability::CapabilityId::SYNC_PULL: Self +pub const radroots_sdk::capability::CapabilityId::SYNC_PUSH: Self +pub const radroots_sdk::capability::CapabilityId::TRADE_COMMANDS: Self +pub const radroots_sdk::capability::CapabilityId::TRADE_QUERIES: Self +pub const fn radroots_sdk::capability::CapabilityId::as_str(self) -> &'static str +impl core::fmt::Display for radroots_sdk::capability::CapabilityId +pub fn radroots_sdk::capability::CapabilityId::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::capability::CapabilityReport +impl radroots_sdk::capability::CapabilityReport +pub fn radroots_sdk::capability::CapabilityReport::get(&self, radroots_sdk::capability::CapabilityId) -> core::option::Option<radroots_sdk::capability::CapabilityStatus> +pub fn radroots_sdk::capability::CapabilityReport::iter(&self) -> impl core::iter::traits::exact_size::ExactSizeIterator<Item = &radroots_sdk::capability::CapabilityStatus> +pub struct radroots_sdk::capability::CapabilityStatus +impl radroots_sdk::capability::CapabilityStatus +pub const fn radroots_sdk::capability::CapabilityStatus::availability(self) -> radroots_sdk::capability::Availability +pub const fn radroots_sdk::capability::CapabilityStatus::id(self) -> radroots_sdk::capability::CapabilityId +pub const fn radroots_sdk::capability::CapabilityStatus::is_compiled(self) -> bool +pub const fn radroots_sdk::capability::CapabilityStatus::is_configured(self) -> bool +pub const fn radroots_sdk::capability::CapabilityStatus::maturity(self) -> radroots_sdk::capability::Maturity +pub mod radroots_sdk::client +pub struct radroots_sdk::client::Client +impl radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::capabilities(&self) -> radroots_sdk::capability::CapabilityReport +pub async fn radroots_sdk::client::Client::close(&self) -> radroots_sdk::error::Result<()> +pub fn radroots_sdk::client::Client::farm(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::farm::Operations<'_>>> +pub fn radroots_sdk::client::Client::is_closed(&self) -> bool +pub fn radroots_sdk::client::Client::listing(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::listing::Operations<'_>>> +pub fn radroots_sdk::client::Client::signer(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_signing::signer::Signer>> +pub fn radroots_sdk::client::Client::signing(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::signing::Operations<'_>>> +pub fn radroots_sdk::client::Client::sink(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_transport::sink::EventSink>> +pub fn radroots_sdk::client::Client::source(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_transport::source::EventSource>> +pub fn radroots_sdk::client::Client::storage(&self) -> radroots_sdk::error::Result<&dyn radroots_storage::Storage> +pub async fn radroots_sdk::client::Client::storage_integrity(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::IntegrityStatus> +pub fn radroots_sdk::client::Client::storage_operations(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::Operations<'_>> +pub async fn radroots_sdk::client::Client::storage_status(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::Status> +pub fn radroots_sdk::client::Client::sync(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::sync::Operations<'_>>> +pub fn radroots_sdk::client::Client::trade(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::trade::Operations<'_>>> +impl core::fmt::Debug for radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::client::ClientBuilder +impl radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::build(self) -> radroots_sdk::error::Result<radroots_sdk::client::Client> +pub fn radroots_sdk::client::ClientBuilder::capability_availability(self, radroots_sdk::capability::CapabilityId, radroots_sdk::capability::Availability) -> Self +pub fn radroots_sdk::client::ClientBuilder::host_sync(self, radroots_sdk::sync::HostPolicy) -> Self +pub fn radroots_sdk::client::ClientBuilder::memory(radroots_storage::event::SourceGeneration) -> Self +pub fn radroots_sdk::client::ClientBuilder::memory_default() -> Self +pub fn radroots_sdk::client::ClientBuilder::new() -> Self +pub fn radroots_sdk::client::ClientBuilder::nostr(self, radroots_sdk::transport::NostrSlot) -> Self +pub fn radroots_sdk::client::ClientBuilder::signer(self, alloc::sync::Arc<dyn radroots_signing::signer::Signer>) -> Self +pub fn radroots_sdk::client::ClientBuilder::signing(self, radroots_sdk::signing::Provider) -> Self +pub fn radroots_sdk::client::ClientBuilder::sink(self, alloc::sync::Arc<dyn radroots_transport::sink::EventSink>) -> Self +pub fn radroots_sdk::client::ClientBuilder::source(self, alloc::sync::Arc<dyn radroots_transport::source::EventSource>) -> Self +pub async fn radroots_sdk::client::ClientBuilder::sqlite(radroots_sdk::storage::SqliteOptions) -> radroots_sdk::error::Result<Self> +pub fn radroots_sdk::client::ClientBuilder::storage(self, alloc::sync::Arc<dyn radroots_storage::Storage>) -> Self +pub fn radroots_sdk::client::ClientBuilder::sync_engine(self, radroots_sync::engine::Engine) -> Self +impl core::fmt::Debug for radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub mod radroots_sdk::diagnostics +pub struct radroots_sdk::diagnostics::Report +impl radroots_sdk::diagnostics::Report +pub const fn radroots_sdk::diagnostics::Report::capabilities(&self) -> &radroots_sdk::capability::CapabilityReport +pub const fn radroots_sdk::diagnostics::Report::storage(&self) -> radroots_storage::status::StorageStatus +pub async fn radroots_sdk::diagnostics::inspect(&radroots_sdk::client::Client) -> radroots_sdk::error::Result<radroots_sdk::diagnostics::Report> +pub mod radroots_sdk::error +#[non_exhaustive] pub enum radroots_sdk::error::ErrorKind +pub radroots_sdk::error::ErrorKind::ClientClosed +pub radroots_sdk::error::ErrorKind::ClientClosing +pub radroots_sdk::error::ErrorKind::CloseInProgress +pub radroots_sdk::error::ErrorKind::InvalidHostConfiguration +pub radroots_sdk::error::ErrorKind::MissingStorage +pub radroots_sdk::error::ErrorKind::SharedOperationUnavailable +pub radroots_sdk::error::ErrorKind::StorageBusy +pub radroots_sdk::error::ErrorKind::StorageCloseFailed +pub radroots_sdk::error::ErrorKind::StorageInspectionFailed +pub radroots_sdk::error::ErrorKind::StorageOpenFailed +pub radroots_sdk::error::ErrorKind::StorageSchemaTooNew +pub radroots_sdk::error::ErrorKind::StorageUnsupportedSchema +impl radroots_sdk::error::ErrorKind +pub const radroots_sdk::error::ErrorKind::ALL: &'static [Self] +pub const fn radroots_sdk::error::ErrorKind::descriptor(self) -> radroots_sdk::error::ErrorDescriptor +pub struct radroots_sdk::error::Error +impl radroots_sdk::error::Error +pub const fn radroots_sdk::error::Error::descriptor(&self) -> radroots_sdk::error::ErrorDescriptor +pub const fn radroots_sdk::error::Error::kind(&self) -> radroots_sdk::error::ErrorKind +pub fn radroots_sdk::error::Error::to_report(&self) -> radroots_protocol::error::v1::ErrorReport +impl core::error::Error for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::error::ErrorDescriptor +impl radroots_sdk::error::ErrorDescriptor +pub const fn radroots_sdk::error::ErrorDescriptor::capability(self) -> core::option::Option<radroots_sdk::capability::CapabilityId> +pub const fn radroots_sdk::error::ErrorDescriptor::class(self) -> radroots_protocol::error::v1::Class +pub const fn radroots_sdk::error::ErrorDescriptor::code(self) -> radroots_protocol::error::v1::KnownCode +pub const fn radroots_sdk::error::ErrorDescriptor::kind(self) -> radroots_sdk::error::ErrorKind +pub const fn radroots_sdk::error::ErrorDescriptor::message(self) -> &'static str +pub const fn radroots_sdk::error::ErrorDescriptor::operation(self) -> core::option::Option<radroots_protocol::runtime::v1::OperationId> +pub const fn radroots_sdk::error::ErrorDescriptor::recovery_actions(self) -> &'static [radroots_protocol::error::v1::RecoveryAction] +pub const fn radroots_sdk::error::ErrorDescriptor::retryable(self) -> bool +pub const fn radroots_sdk::error::ErrorDescriptor::safe_detail_keys(self) -> &'static [&'static str] +pub const radroots_sdk::error::CATALOG: &[radroots_sdk::error::ErrorDescriptor] +pub type radroots_sdk::error::Result<T> = core::result::Result<T, radroots_sdk::error::Error> +pub mod radroots_sdk::farm +#[non_exhaustive] pub enum radroots_sdk::farm::PrepareErrorKind +pub radroots_sdk::farm::PrepareErrorKind::Coordinate +pub radroots_sdk::farm::PrepareErrorKind::Draft +pub radroots_sdk::farm::PrepareErrorKind::Encode +pub radroots_sdk::farm::PrepareErrorKind::UnauthorizedActor +pub struct radroots_sdk::farm::EnqueueRequest +impl radroots_sdk::farm::EnqueueRequest +pub const fn radroots_sdk::farm::EnqueueRequest::new(radroots_sync::policy::SyncId, radroots_storage::journal::IdempotencyKey, radroots_sdk::farm::Plan, radroots_sdk::transport::Profile, u64, radroots_signing::request::CancellationPolicy) -> Self +pub struct radroots_sdk::farm::Operations<'a> +impl<'a> radroots_sdk::farm::Operations<'a> +pub async fn radroots_sdk::farm::Operations<'a>::enqueue(&self, radroots_sdk::farm::EnqueueRequest) -> core::result::Result<radroots_sync::push::PushStatus, radroots_sync::policy::Error> +pub struct radroots_sdk::farm::Plan +impl radroots_sdk::farm::Plan +pub const fn radroots_sdk::farm::Plan::actor(&self) -> &radroots_signing::actor::Actor +pub const fn radroots_sdk::farm::Plan::authored_event(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_sdk::farm::Plan::coordinate(&self) -> &radroots_event::id::AddressableCoordinate +pub struct radroots_sdk::farm::PrepareError +impl radroots_sdk::farm::PrepareError +pub const fn radroots_sdk::farm::PrepareError::kind(&self) -> radroots_sdk::farm::PrepareErrorKind +impl core::error::Error for radroots_sdk::farm::PrepareError +pub fn radroots_sdk::farm::PrepareError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::farm::PrepareError +pub fn radroots_sdk::farm::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::farm::PrepareError +pub fn radroots_sdk::farm::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::farm::PrepareRequest +impl radroots_sdk::farm::PrepareRequest +pub const fn radroots_sdk::farm::PrepareRequest::new(radroots_signing::actor::Actor, radroots_event::farm::Farm, u64) -> Self +pub fn radroots_sdk::farm::prepare(radroots_sdk::farm::PrepareRequest) -> core::result::Result<radroots_sdk::farm::Plan, radroots_sdk::farm::PrepareError> +pub mod radroots_sdk::listing +#[non_exhaustive] pub enum radroots_sdk::listing::Action +pub radroots_sdk::listing::Action::Publish +pub radroots_sdk::listing::Action::Update +#[non_exhaustive] pub enum radroots_sdk::listing::PrepareErrorKind +pub radroots_sdk::listing::PrepareErrorKind::Edit +pub radroots_sdk::listing::PrepareErrorKind::Mutation +pub radroots_sdk::listing::PrepareErrorKind::UnauthorizedActor +pub enum radroots_sdk::listing::RadrootsOperationalListingEditError +pub radroots_sdk::listing::RadrootsOperationalListingEditError::DuplicateBinId +pub radroots_sdk::listing::RadrootsOperationalListingEditError::DuplicateBinId::bin_id: radroots_event::id::InventoryBinId +pub radroots_sdk::listing::RadrootsOperationalListingEditError::FarmPubkeyMismatch +pub radroots_sdk::listing::RadrootsOperationalListingEditError::FarmPubkeyMismatch::actual_pubkey: radroots_identity::key::PublicKey +pub radroots_sdk::listing::RadrootsOperationalListingEditError::FarmPubkeyMismatch::expected_pubkey: radroots_identity::key::PublicKey +pub radroots_sdk::listing::RadrootsOperationalListingEditError::InvalidClassifiedListingAddress(radroots_event::id::ParseError) +pub radroots_sdk::listing::RadrootsOperationalListingEditError::InvalidFarmPubkey(radroots_identity::error::Error) +pub radroots_sdk::listing::RadrootsOperationalListingEditError::InvalidModel(radroots_event::trade::validation::OperationalListingValidationError) +pub radroots_sdk::listing::RadrootsOperationalListingEditError::MissingPrimaryBin +pub radroots_sdk::listing::RadrootsOperationalListingEditError::MissingPrimaryBin::primary_bin_id: radroots_event::id::InventoryBinId +impl core::error::Error for radroots_sdk::listing::RadrootsOperationalListingEditError +impl core::fmt::Display for radroots_sdk::listing::RadrootsOperationalListingEditError +pub fn radroots_sdk::listing::RadrootsOperationalListingEditError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub enum radroots_sdk::listing::RadrootsOperationalListingLifecycleState +pub radroots_sdk::listing::RadrootsOperationalListingLifecycleState::Draft +pub radroots_sdk::listing::RadrootsOperationalListingLifecycleState::Published +pub enum radroots_sdk::listing::RadrootsOperationalListingMutation +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Archive +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Archive::listing_addr: radroots_event::id::ClassifiedListingAddress +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Publish +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Publish::draft: radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit +pub radroots_sdk::listing::RadrootsOperationalListingMutation::SaveDraft +pub radroots_sdk::listing::RadrootsOperationalListingMutation::SaveDraft::draft: radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Update +pub radroots_sdk::listing::RadrootsOperationalListingMutation::Update::draft: radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit +impl radroots_sdk::listing::RadrootsOperationalListingMutation +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::archive(radroots_event::id::ClassifiedListingAddress) -> Self +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::canonical_draft(&self) -> core::result::Result<&radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit, radroots_sdk::listing::RadrootsOperationalListingMutationError> +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::lifecycle_state(&self) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingLifecycleState, radroots_sdk::listing::RadrootsOperationalListingMutationError> +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::listing_addr(&self) -> core::result::Result<&radroots_event::id::ClassifiedListingAddress, radroots_sdk::listing::RadrootsOperationalListingMutationError> +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::publish(radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit) -> Self +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::save_draft(radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit) -> Self +pub fn radroots_sdk::listing::RadrootsOperationalListingMutation::update(radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit) -> Self +pub enum radroots_sdk::listing::RadrootsOperationalListingMutationError +pub radroots_sdk::listing::RadrootsOperationalListingMutationError::AuthoredPlan(radroots_event::draft::DraftError) +pub radroots_sdk::listing::RadrootsOperationalListingMutationError::EncodeListing(alloc::string::String) +pub radroots_sdk::listing::RadrootsOperationalListingMutationError::UnsupportedMutation +impl core::error::Error for radroots_sdk::listing::RadrootsOperationalListingMutationError +impl core::fmt::Display for radroots_sdk::listing::RadrootsOperationalListingMutationError +pub fn radroots_sdk::listing::RadrootsOperationalListingMutationError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::listing::EnqueueRequest +impl radroots_sdk::listing::EnqueueRequest +pub const fn radroots_sdk::listing::EnqueueRequest::new(radroots_sync::policy::SyncId, radroots_storage::journal::IdempotencyKey, radroots_sdk::listing::Plan, radroots_sdk::transport::Profile, u64, radroots_signing::request::CancellationPolicy) -> Self +pub struct radroots_sdk::listing::Operations<'a> +impl<'a> radroots_sdk::listing::Operations<'a> +pub async fn radroots_sdk::listing::Operations<'a>::enqueue(&self, radroots_sdk::listing::EnqueueRequest) -> core::result::Result<radroots_sync::push::PushStatus, radroots_sync::policy::Error> +pub struct radroots_sdk::listing::Plan +impl radroots_sdk::listing::Plan +pub const fn radroots_sdk::listing::Plan::action(&self) -> radroots_sdk::listing::Action +pub const fn radroots_sdk::listing::Plan::actor(&self) -> &radroots_signing::actor::Actor +pub const fn radroots_sdk::listing::Plan::address(&self) -> &radroots_event::id::ClassifiedListingAddress +pub const fn radroots_sdk::listing::Plan::authored_event(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_sdk::listing::Plan::lifecycle(&self) -> radroots_sdk::listing::RadrootsOperationalListingLifecycleState +pub struct radroots_sdk::listing::PrepareError +impl radroots_sdk::listing::PrepareError +pub const fn radroots_sdk::listing::PrepareError::kind(&self) -> radroots_sdk::listing::PrepareErrorKind +impl core::error::Error for radroots_sdk::listing::PrepareError +pub fn radroots_sdk::listing::PrepareError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::listing::PrepareError +pub fn radroots_sdk::listing::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::listing::PrepareError +pub fn radroots_sdk::listing::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::listing::PrepareRequest +impl radroots_sdk::listing::PrepareRequest +pub const fn radroots_sdk::listing::PrepareRequest::publish(radroots_signing::actor::Actor, radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1, u64) -> Self +pub const fn radroots_sdk::listing::PrepareRequest::update(radroots_signing::actor::Actor, radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1, u64) -> Self +pub struct radroots_sdk::listing::RadrootsClassifiedListingAddressParts +pub radroots_sdk::listing::RadrootsClassifiedListingAddressParts::address: radroots_event::id::ClassifiedListingAddress +pub radroots_sdk::listing::RadrootsClassifiedListingAddressParts::kind: u32 +pub radroots_sdk::listing::RadrootsClassifiedListingAddressParts::listing_id: radroots_event::id::DTag +pub radroots_sdk::listing::RadrootsClassifiedListingAddressParts::seller_pubkey: radroots_identity::key::PublicKey +impl radroots_sdk::listing::RadrootsClassifiedListingAddressParts +pub fn radroots_sdk::listing::RadrootsClassifiedListingAddressParts::parse(impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event::id::ParseError> +pub struct radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit +impl radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit +pub fn radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit::listing(&self) -> &radroots_event::listing::operational::OperationalListing +pub fn radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit::new(radroots_event::listing::operational::OperationalListing, radroots_identity::key::PublicKey) -> core::result::Result<Self, radroots_sdk::listing::RadrootsOperationalListingEditError> +pub fn radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit::public_listing_addr(&self) -> &radroots_event::id::ClassifiedListingAddress +pub fn radroots_sdk::listing::RadrootsOperationalListingCanonicalEdit::seller_pubkey(&self) -> &radroots_identity::key::PublicKey +pub struct radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1 +pub radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1::listing: radroots_event::listing::operational::OperationalListing +impl radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1 +pub fn radroots_sdk::listing::RadrootsOperationalListingEditDocumentV1::new(radroots_event::listing::operational::OperationalListing) -> Self +pub struct radroots_sdk::listing::RadrootsOperationalListingSubtotal +pub radroots_sdk::listing::RadrootsOperationalListingSubtotal::price_amount: radroots_core::money::Money +pub radroots_sdk::listing::RadrootsOperationalListingSubtotal::price_currency: radroots_core::currency::Currency +pub radroots_sdk::listing::RadrootsOperationalListingSubtotal::quantity_amount: radroots_core::decimal::Decimal +pub radroots_sdk::listing::RadrootsOperationalListingSubtotal::quantity_unit: radroots_core::unit::Unit +pub struct radroots_sdk::listing::RadrootsOperationalListingTotal +pub radroots_sdk::listing::RadrootsOperationalListingTotal::price_amount: radroots_core::money::Money +pub radroots_sdk::listing::RadrootsOperationalListingTotal::price_currency: radroots_core::currency::Currency +pub radroots_sdk::listing::RadrootsOperationalListingTotal::quantity_amount: radroots_core::decimal::Decimal +pub radroots_sdk::listing::RadrootsOperationalListingTotal::quantity_unit: radroots_core::unit::Unit +pub struct radroots_sdk::listing::RadrootsOperationalListingTradeProjection +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::availability: radroots_event::listing::operational::OperationalListingAvailability +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::bin_quantity: radroots_core::quantity::Quantity +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::delivery_method: radroots_event::listing::operational::OperationalListingDeliveryMethod +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::description: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::inventory_available: radroots_core::decimal::Decimal +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::listing: radroots_event::listing::operational::OperationalListing +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::listing_addr: radroots_event::id::ClassifiedListingAddress +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::listing_id: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::location: radroots_event::listing::operational::OperationalListingPublicLocation +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::primary_bin_id: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::product_type: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::seller_pubkey: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::title: alloc::string::String +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::unit: radroots_core::unit::Unit +pub radroots_sdk::listing::RadrootsOperationalListingTradeProjection::unit_price: radroots_core::money::Money +pub struct radroots_sdk::listing::RadrootsPublicClassifiedListingAddress +pub radroots_sdk::listing::RadrootsPublicClassifiedListingAddress::address: radroots_event::id::ClassifiedListingAddress +pub radroots_sdk::listing::RadrootsPublicClassifiedListingAddress::kind: u32 +pub radroots_sdk::listing::RadrootsPublicClassifiedListingAddress::listing_id: radroots_event::id::DTag +pub radroots_sdk::listing::RadrootsPublicClassifiedListingAddress::seller_pubkey: radroots_identity::key::PublicKey +impl radroots_sdk::listing::RadrootsPublicClassifiedListingAddress +pub fn radroots_sdk::listing::RadrootsPublicClassifiedListingAddress::parse(impl core::convert::AsRef<str>) -> core::result::Result<Self, radroots_event::id::ParseError> +pub trait radroots_sdk::listing::BinPricingTryExt +pub fn radroots_sdk::listing::BinPricingTryExt::try_subtotal_for_count(&self, u32) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingSubtotal, radroots_core::quantity_price::Error> +pub fn radroots_sdk::listing::BinPricingTryExt::try_total_for_count(&self, u32) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingTotal, radroots_core::quantity_price::Error> +impl radroots_sdk::listing::BinPricingTryExt for radroots_event::listing::operational::OperationalListingBin +pub fn radroots_event::listing::operational::OperationalListingBin::try_subtotal_for_count(&self, u32) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingSubtotal, radroots_core::quantity_price::Error> +pub fn radroots_event::listing::operational::OperationalListingBin::try_total_for_count(&self, u32) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingTotal, radroots_core::quantity_price::Error> +pub fn radroots_sdk::listing::parse_classified_listing_address(impl core::convert::AsRef<str>) -> core::result::Result<radroots_sdk::listing::RadrootsClassifiedListingAddressParts, radroots_event::id::ParseError> +pub fn radroots_sdk::listing::parse_operational_listing_event(&radroots_event::envelope::EventEnvelope) -> core::result::Result<radroots_event::listing::operational::OperationalListing, radroots_event::listing::operational::OperationalListingParseError> +pub fn radroots_sdk::listing::parse_public_classified_listing_address(impl core::convert::AsRef<str>) -> core::result::Result<radroots_sdk::listing::RadrootsPublicClassifiedListingAddress, radroots_event::id::ParseError> +pub fn radroots_sdk::listing::prepare(radroots_sdk::listing::PrepareRequest) -> core::result::Result<radroots_sdk::listing::Plan, radroots_sdk::listing::PrepareError> +pub fn radroots_sdk::listing::validate_operational_listing_event(&radroots_event_codec::verification::v1::RadrootsSignatureVerifiedEvent) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingTradeProjection, radroots_event::trade::validation::OperationalListingValidationError> +pub fn radroots_sdk::listing::validate_operational_listing_model(radroots_event::listing::operational::OperationalListing, &radroots_identity::key::PublicKey) -> core::result::Result<radroots_sdk::listing::RadrootsOperationalListingTradeProjection, radroots_event::trade::validation::OperationalListingValidationError> +pub mod radroots_sdk::signing +pub use radroots_sdk::signing::AuthorizationHeader +pub use radroots_sdk::signing::BlossomAuthorizationPlan +#[non_exhaustive] pub enum radroots_sdk::signing::BlossomSigningError +pub radroots_sdk::signing::BlossomSigningError::Encoding(radroots_nostr::blossom::AuthorizationError) +pub radroots_sdk::signing::BlossomSigningError::Signing(radroots_signing::error::Error) +pub radroots_sdk::signing::BlossomSigningError::WrongPurpose +impl core::error::Error for radroots_sdk::signing::BlossomSigningError +pub fn radroots_sdk::signing::BlossomSigningError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Display for radroots_sdk::signing::BlossomSigningError +pub fn radroots_sdk::signing::BlossomSigningError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +#[non_exhaustive] pub enum radroots_sdk::signing::Mode +pub radroots_sdk::signing::Mode::Host +pub radroots_sdk::signing::Mode::Local +pub radroots_sdk::signing::Mode::Nip46 +pub struct radroots_sdk::signing::Operations<'a> +impl<'a> radroots_sdk::signing::Operations<'a> +pub async fn radroots_sdk::signing::Operations<'a>::authorize_blossom_upload(&self, radroots_signing::request::SignRequest) -> core::result::Result<radroots_nostr::blossom::AuthorizationHeader, radroots_sdk::signing::BlossomSigningError> +pub async fn radroots_sdk::signing::Operations<'a>::sign(&self, radroots_signing::request::SignRequest) -> core::result::Result<radroots_signing::receipt::SignReceipt, radroots_signing::error::Error> +impl core::fmt::Debug for radroots_sdk::signing::Operations<'_> +pub fn radroots_sdk::signing::Operations<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::signing::Provider +impl radroots_sdk::signing::Provider +pub fn radroots_sdk::signing::Provider::as_signer(&self) -> &dyn radroots_signing::signer::Signer +pub fn radroots_sdk::signing::Provider::host(alloc::sync::Arc<dyn radroots_signing::signer::Signer>) -> Self +pub fn radroots_sdk::signing::Provider::local(radroots_nostr::signing::LocalSigner) -> Self +pub const fn radroots_sdk::signing::Provider::mode(&self) -> radroots_sdk::signing::Mode +pub fn radroots_sdk::signing::Provider::nip46(alloc::sync::Arc<dyn radroots_signing::signer::Signer>) -> Self +pub async fn radroots_sdk::signing::Provider::sign(&self, radroots_signing::request::SignRequest) -> core::result::Result<radroots_signing::receipt::SignReceipt, radroots_signing::error::Error> +pub async fn radroots_sdk::signing::Provider::status(&self) -> core::result::Result<radroots_signing::status::SignerStatus, radroots_signing::error::Error> +impl core::fmt::Debug for radroots_sdk::signing::Provider +pub fn radroots_sdk::signing::Provider::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub fn radroots_sdk::signing::blossom_upload_request(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::BlossomAuthorizationPlan, radroots_signing::request::SignPolicy) -> core::result::Result<radroots_signing::request::SignRequest, radroots_signing::error::Error> +pub mod radroots_sdk::storage +pub struct radroots_sdk::storage::Operations<'a> +impl<'a> radroots_sdk::storage::Operations<'a> +pub async fn radroots_sdk::storage::Operations<'a>::begin_backup(&self, radroots_storage::backup::BackupPlan) -> core::result::Result<radroots_storage::backup::BackupOperation, radroots_storage::error::Error> +pub async fn radroots_sdk::storage::Operations<'a>::begin_restore(&self, radroots_storage::backup::RestorePlan) -> core::result::Result<radroots_storage::backup::RestoreOperation, radroots_storage::error::Error> +pub async fn radroots_sdk::storage::Operations<'a>::integrity(&self) -> core::result::Result<radroots_sdk::storage::IntegrityStatus, radroots_storage::error::Error> +pub async fn radroots_sdk::storage::Operations<'a>::status(&self) -> core::result::Result<radroots_sdk::storage::Status, radroots_storage::error::Error> +pub async fn radroots_sdk::storage::Operations<'a>::transition_backup(&self, radroots_storage::backup::BackupId, radroots_storage::backup::ReliabilityRevision, radroots_storage::backup::BackupTransition, u64) -> core::result::Result<radroots_storage::backup::BackupOperation, radroots_storage::error::Error> +pub async fn radroots_sdk::storage::Operations<'a>::transition_restore(&self, radroots_storage::backup::BackupId, radroots_storage::backup::ReliabilityRevision, radroots_storage::backup::RestoreTransition, u64) -> core::result::Result<radroots_storage::backup::RestoreOperation, radroots_storage::error::Error> +impl core::fmt::Debug for radroots_sdk::storage::Operations<'_> +pub fn radroots_sdk::storage::Operations<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub type radroots_sdk::storage::IntegrityStatus = radroots_storage::status::IntegrityStatus +pub type radroots_sdk::storage::SqliteOpenMode = radroots_storage_sqlite::open::OpenMode +pub type radroots_sdk::storage::SqliteOptions = radroots_storage_sqlite::config::OpenOptions +pub type radroots_sdk::storage::SqlitePaths = radroots_storage_sqlite::open::Paths +pub type radroots_sdk::storage::Status = radroots_storage::status::StorageStatus +pub mod radroots_sdk::sync +pub struct radroots_sdk::sync::HostPolicy +impl radroots_sdk::sync::HostPolicy +pub fn radroots_sdk::sync::HostPolicy::new(u64, u64, u64) -> core::result::Result<Self, radroots_sync::policy::Error> +pub fn radroots_sdk::sync::HostPolicy::standard() -> Self +impl core::default::Default for radroots_sdk::sync::HostPolicy +pub fn radroots_sdk::sync::HostPolicy::default() -> Self +pub struct radroots_sdk::sync::Operations<'a> +impl<'a> radroots_sdk::sync::Operations<'a> +pub async fn radroots_sdk::sync::Operations<'a>::admit_signed(&self, radroots_sync::policy::SyncId) -> core::result::Result<radroots_sync::push::AdmissionRunReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::cancel_push(&self, radroots_sync::policy::SyncId) -> core::result::Result<radroots_sync::push::PushCancellationReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::deliver_push(&self, radroots_sync::policy::SyncId) -> core::result::Result<radroots_sync::push::DeliveryExecutionReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::ingest(&self, radroots_transport::source::ObservedEvent, &dyn radroots_sync::ingest::AdmissionPolicy) -> core::result::Result<radroots_sync::ingest::IngestReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::ingest_batch(&self, alloc::vec::Vec<radroots_transport::source::ObservedEvent>, &dyn radroots_sync::ingest::AdmissionPolicy) -> radroots_sync::ingest::IngestBatchReceipt +pub async fn radroots_sdk::sync::Operations<'a>::prepare_push(&self, radroots_sync::push::PushRequest) -> core::result::Result<radroots_sync::push::PushPreparation, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::pull(&self, radroots_sync::pull::PullRequest, &dyn radroots_sync::ingest::AdmissionPolicy) -> core::result::Result<radroots_sync::pull::PullReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::push_status(&self, radroots_sync::policy::SyncId) -> core::result::Result<core::option::Option<radroots_sync::push::PushStatus>, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::refresh_projection(&self, radroots_sync::projection::RefreshRequest, &dyn radroots_sync::projection::Reducer) -> core::result::Result<radroots_sync::projection::RefreshReceipt, radroots_sync::policy::Error> +pub fn radroots_sdk::sync::Operations<'a>::retry_decision(&self, &radroots_storage::authored_delivery::AuthoredDeliveryPlan, u64) -> core::result::Result<radroots_protocol::runtime::v1::SyncRetryDecision, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::sign_prepared(&self, radroots_sync::push::PushRequest) -> core::result::Result<radroots_sync::push::SigningRunReceipt, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::status(&self, &[radroots_storage::projection::ProjectionId]) -> core::result::Result<radroots_sync::status::SyncStatus, radroots_sync::policy::Error> +pub async fn radroots_sdk::sync::Operations<'a>::submit_push(&self, radroots_sync::push::PushRequest) -> core::result::Result<radroots_sync::push::PushStatus, radroots_sync::policy::Error> +impl core::fmt::Debug for radroots_sdk::sync::Operations<'_> +pub fn radroots_sdk::sync::Operations<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub mod radroots_sdk::trade +#[non_exhaustive] pub enum radroots_sdk::trade::PrepareErrorKind +pub radroots_sdk::trade::PrepareErrorKind::CanonicalMutation +pub radroots_sdk::trade::PrepareErrorKind::Draft +pub radroots_sdk::trade::PrepareErrorKind::Encode +pub radroots_sdk::trade::PrepareErrorKind::UnauthorizedActor +pub radroots_sdk::trade::PrepareErrorKind::Workflow +#[non_exhaustive] pub enum radroots_sdk::trade::PrivateTermsError +pub radroots_sdk::trade::PrivateTermsError::EvidenceMismatch +pub radroots_sdk::trade::PrivateTermsError::NotRequired +pub radroots_sdk::trade::PrivateTermsError::Storage +pub struct radroots_sdk::trade::EnqueueRequest +impl radroots_sdk::trade::EnqueueRequest +pub const fn radroots_sdk::trade::EnqueueRequest::new(radroots_sync::policy::SyncId, radroots_storage::journal::IdempotencyKey, radroots_sdk::trade::Plan, radroots_sdk::transport::Profile, u64, radroots_signing::request::CancellationPolicy) -> Self +pub struct radroots_sdk::trade::Operations<'a> +impl<'a> radroots_sdk::trade::Operations<'a> +pub async fn radroots_sdk::trade::Operations<'a>::enqueue(&self, radroots_sdk::trade::EnqueueRequest) -> core::result::Result<radroots_sync::push::PushStatus, radroots_sync::policy::Error> +pub async fn radroots_sdk::trade::Operations<'a>::private_artifact(&self, radroots_storage::private_artifact::PrivateArtifactId) -> core::result::Result<core::option::Option<radroots_storage::private_artifact::PrivateArtifactMetadata>, radroots_storage::error::Error> +pub async fn radroots_sdk::trade::Operations<'a>::query_visible(&self, radroots_storage::event::EventQuery) -> core::result::Result<radroots_storage::event::EventPage<radroots_storage::event::StoredVisibleEvent>, radroots_storage::error::Error> +pub async fn radroots_sdk::trade::Operations<'a>::verify_private_terms(&self, &radroots_sdk::trade::Plan, radroots_storage::private_artifact::PrivateArtifactId) -> core::result::Result<radroots_storage::private_artifact::PrivateArtifactMetadata, radroots_sdk::trade::PrivateTermsError> +impl core::fmt::Debug for radroots_sdk::trade::Operations<'_> +pub fn radroots_sdk::trade::Operations<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::trade::Plan +impl radroots_sdk::trade::Plan +pub const fn radroots_sdk::trade::Plan::actor(&self) -> &radroots_signing::actor::Actor +pub const fn radroots_sdk::trade::Plan::authored_event(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_sdk::trade::Plan::workflow(&self) -> &radroots_trade::workflow::WorkflowPlan +pub struct radroots_sdk::trade::PrepareError +impl radroots_sdk::trade::PrepareError +pub const fn radroots_sdk::trade::PrepareError::kind(&self) -> radroots_sdk::trade::PrepareErrorKind +impl core::error::Error for radroots_sdk::trade::PrepareError +pub fn radroots_sdk::trade::PrepareError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::trade::PrepareError +pub fn radroots_sdk::trade::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::trade::PrepareError +pub fn radroots_sdk::trade::PrepareError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::trade::PrepareRequest +impl radroots_sdk::trade::PrepareRequest +pub const fn radroots_sdk::trade::PrepareRequest::new(radroots_signing::actor::Actor, radroots_event::trade::TradeMutationEnvelopeV1) -> Self +pub fn radroots_sdk::trade::prepare(radroots_sdk::trade::PrepareRequest) -> core::result::Result<radroots_sdk::trade::Plan, radroots_sdk::trade::PrepareError> +pub fn radroots_sdk::trade::project(radroots_trade::trade_contract_v1::RadrootsTradeReductionInputV1) -> radroots_trade::trade_contract_v1::RadrootsTradeProjectionV1 +pub mod radroots_sdk::transport +pub use radroots_sdk::transport::RelayUrlPolicy +#[non_exhaustive] pub enum radroots_sdk::transport::DaemonAuth +pub radroots_sdk::transport::DaemonAuth::BearerToken(alloc::string::String) +pub radroots_sdk::transport::DaemonAuth::None +impl core::fmt::Debug for radroots_sdk::transport::DaemonAuth +pub fn radroots_sdk::transport::DaemonAuth::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +#[non_exhaustive] pub enum radroots_sdk::transport::DaemonErrorKind +pub radroots_sdk::transport::DaemonErrorKind::Authentication +pub radroots_sdk::transport::DaemonErrorKind::InvalidRequest +pub radroots_sdk::transport::DaemonErrorKind::InvalidResponse +pub radroots_sdk::transport::DaemonErrorKind::Rpc +pub radroots_sdk::transport::DaemonErrorKind::Transport +pub struct radroots_sdk::transport::DaemonConfig +impl radroots_sdk::transport::DaemonConfig +pub fn radroots_sdk::transport::DaemonConfig::new(impl core::convert::Into<alloc::string::String>) -> Self +pub fn radroots_sdk::transport::DaemonConfig::with_auth(self, radroots_sdk::transport::DaemonAuth) -> Self +pub const fn radroots_sdk::transport::DaemonConfig::with_timeout(self, core::time::Duration) -> Self +pub struct radroots_sdk::transport::DaemonDelivery +impl radroots_sdk::transport::DaemonDelivery +pub async fn radroots_sdk::transport::DaemonDelivery::deliver(&self, radroots_event::draft::SignedEvent, radroots_protocol::radrootsd::transport_publish::v5::TargetPolicy, radroots_protocol::radrootsd::transport_publish::v5::DeliveryPolicy, core::option::Option<alloc::string::String>, core::option::Option<u64>) -> core::result::Result<radroots_protocol::radrootsd::transport_publish::v5::EventResponse, radroots_sdk::transport::DaemonError> +pub fn radroots_sdk::transport::DaemonDelivery::new(radroots_sdk::transport::DaemonConfig) -> Self +pub struct radroots_sdk::transport::DaemonError +impl radroots_sdk::transport::DaemonError +pub const fn radroots_sdk::transport::DaemonError::kind(&self) -> radroots_sdk::transport::DaemonErrorKind +impl core::error::Error for radroots_sdk::transport::DaemonError +pub fn radroots_sdk::transport::DaemonError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::transport::DaemonError +pub fn radroots_sdk::transport::DaemonError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::transport::DaemonError +pub fn radroots_sdk::transport::DaemonError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::transport::NostrSlot +impl radroots_sdk::transport::NostrSlot +pub fn radroots_sdk::transport::NostrSlot::clear(&self) +pub fn radroots_sdk::transport::NostrSlot::configure<I, S>(&self, I) -> radroots_sdk::error::Result<()> where I: core::iter::traits::collect::IntoIterator<Item = S>, S: core::convert::AsRef<str> +pub fn radroots_sdk::transport::NostrSlot::new(radroots_transport_nostr::relay::RelayUrlPolicy) -> Self +pub fn radroots_sdk::transport::NostrSlot::targets(&self) -> core::option::Option<radroots_transport::target::TargetSet> +impl core::fmt::Debug for radroots_sdk::transport::NostrSlot +pub fn radroots_sdk::transport::NostrSlot::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl radroots_transport::sink::EventSink for radroots_sdk::transport::NostrSlot +pub fn radroots_sdk::transport::NostrSlot::deliver(&self, radroots_transport::sink::DeliveryRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_transport::sink::DeliveryReceipt, radroots_transport::sink::SinkFailure>> +pub fn radroots_sdk::transport::NostrSlot::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_transport::status::SinkStatus, radroots_transport::error::Error>> +impl radroots_transport::source::EventSource for radroots_sdk::transport::NostrSlot +pub fn radroots_sdk::transport::NostrSlot::fetch(&self, radroots_transport::source::FetchRequest) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_transport::source::FetchPage, radroots_transport::error::Error>> +pub fn radroots_sdk::transport::NostrSlot::status(&self) -> radroots_transport::source::BoxFuture<'_, core::result::Result<radroots_transport::status::SourceStatus, radroots_transport::error::Error>> +pub struct radroots_sdk::transport::Profile +impl radroots_sdk::transport::Profile +pub fn radroots_sdk::transport::Profile::delivery(radroots_transport::target::TargetSet, radroots_transport::policy::SatisfactionPolicy) -> core::result::Result<Self, radroots_transport::error::Error> +pub const fn radroots_sdk::transport::Profile::is_local_only(&self) -> bool +pub const fn radroots_sdk::transport::Profile::local_only() -> Self +pub const fn radroots_sdk::transport::Profile::satisfaction(&self) -> core::option::Option<&radroots_transport::policy::SatisfactionPolicy> +pub const fn radroots_sdk::transport::Profile::sink_status(&self) -> core::option::Option<&radroots_transport::status::SinkStatus> +pub const fn radroots_sdk::transport::Profile::source_status(&self) -> core::option::Option<&radroots_transport::status::SourceStatus> +pub const fn radroots_sdk::transport::Profile::targets(&self) -> core::option::Option<&radroots_transport::target::TargetSet> +pub fn radroots_sdk::transport::Profile::unavailable_preview(radroots_transport::id::TransportId) -> Self +impl core::default::Default for radroots_sdk::transport::Profile +pub fn radroots_sdk::transport::Profile::default() -> Self +pub struct radroots_sdk::Client +impl radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::capabilities(&self) -> radroots_sdk::capability::CapabilityReport +pub async fn radroots_sdk::client::Client::close(&self) -> radroots_sdk::error::Result<()> +pub fn radroots_sdk::client::Client::farm(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::farm::Operations<'_>>> +pub fn radroots_sdk::client::Client::is_closed(&self) -> bool +pub fn radroots_sdk::client::Client::listing(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::listing::Operations<'_>>> +pub fn radroots_sdk::client::Client::signer(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_signing::signer::Signer>> +pub fn radroots_sdk::client::Client::signing(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::signing::Operations<'_>>> +pub fn radroots_sdk::client::Client::sink(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_transport::sink::EventSink>> +pub fn radroots_sdk::client::Client::source(&self) -> radroots_sdk::error::Result<core::option::Option<&dyn radroots_transport::source::EventSource>> +pub fn radroots_sdk::client::Client::storage(&self) -> radroots_sdk::error::Result<&dyn radroots_storage::Storage> +pub async fn radroots_sdk::client::Client::storage_integrity(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::IntegrityStatus> +pub fn radroots_sdk::client::Client::storage_operations(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::Operations<'_>> +pub async fn radroots_sdk::client::Client::storage_status(&self) -> radroots_sdk::error::Result<radroots_sdk::storage::Status> +pub fn radroots_sdk::client::Client::sync(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::sync::Operations<'_>>> +pub fn radroots_sdk::client::Client::trade(&self) -> radroots_sdk::error::Result<core::option::Option<radroots_sdk::trade::Operations<'_>>> +impl core::fmt::Debug for radroots_sdk::client::Client +pub fn radroots_sdk::client::Client::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::ClientBuilder +impl radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::build(self) -> radroots_sdk::error::Result<radroots_sdk::client::Client> +pub fn radroots_sdk::client::ClientBuilder::capability_availability(self, radroots_sdk::capability::CapabilityId, radroots_sdk::capability::Availability) -> Self +pub fn radroots_sdk::client::ClientBuilder::host_sync(self, radroots_sdk::sync::HostPolicy) -> Self +pub fn radroots_sdk::client::ClientBuilder::memory(radroots_storage::event::SourceGeneration) -> Self +pub fn radroots_sdk::client::ClientBuilder::memory_default() -> Self +pub fn radroots_sdk::client::ClientBuilder::new() -> Self +pub fn radroots_sdk::client::ClientBuilder::nostr(self, radroots_sdk::transport::NostrSlot) -> Self +pub fn radroots_sdk::client::ClientBuilder::signer(self, alloc::sync::Arc<dyn radroots_signing::signer::Signer>) -> Self +pub fn radroots_sdk::client::ClientBuilder::signing(self, radroots_sdk::signing::Provider) -> Self +pub fn radroots_sdk::client::ClientBuilder::sink(self, alloc::sync::Arc<dyn radroots_transport::sink::EventSink>) -> Self +pub fn radroots_sdk::client::ClientBuilder::source(self, alloc::sync::Arc<dyn radroots_transport::source::EventSource>) -> Self +pub async fn radroots_sdk::client::ClientBuilder::sqlite(radroots_sdk::storage::SqliteOptions) -> radroots_sdk::error::Result<Self> +pub fn radroots_sdk::client::ClientBuilder::storage(self, alloc::sync::Arc<dyn radroots_storage::Storage>) -> Self +pub fn radroots_sdk::client::ClientBuilder::sync_engine(self, radroots_sync::engine::Engine) -> Self +impl core::fmt::Debug for radroots_sdk::client::ClientBuilder +pub fn radroots_sdk::client::ClientBuilder::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_sdk::Error +impl radroots_sdk::error::Error +pub const fn radroots_sdk::error::Error::descriptor(&self) -> radroots_sdk::error::ErrorDescriptor +pub const fn radroots_sdk::error::Error::kind(&self) -> radroots_sdk::error::ErrorKind +pub fn radroots_sdk::error::Error::to_report(&self) -> radroots_protocol::error::v1::ErrorReport +impl core::error::Error for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)> +impl core::fmt::Debug for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::fmt::Display for radroots_sdk::error::Error +pub fn radroots_sdk::error::Error::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub type radroots_sdk::Result<T> = core::result::Result<T, radroots_sdk::error::Error> diff --git a/docs/api/radroots_signing.txt b/docs/api/radroots_signing.txt @@ -170,6 +170,9 @@ pub mod radroots_signing::request #[non_exhaustive] pub enum radroots_signing::request::CancellationPolicy pub radroots_signing::request::CancellationPolicy::LocalCooperative pub radroots_signing::request::CancellationPolicy::PreservePublishedRequest +#[non_exhaustive] pub enum radroots_signing::request::SigningPurpose +pub radroots_signing::request::SigningPurpose::AuthoredEvent +pub radroots_signing::request::SigningPurpose::BlossomUploadAuthorization pub struct radroots_signing::request::CancellationSignal(_) impl radroots_signing::request::CancellationSignal pub fn radroots_signing::request::CancellationSignal::cancel(&self) @@ -189,17 +192,27 @@ pub fn radroots_signing::request::SignPolicy::deserialize<D>(D) -> core::result: pub struct radroots_signing::request::SignRequest impl radroots_signing::request::SignRequest pub const fn radroots_signing::request::SignRequest::actor(&self) -> &radroots_signing::actor::Actor -pub const fn radroots_signing::request::SignRequest::authorization(&self) -> radroots_signing::authorization::CurrentAuthoringDecision +pub const fn radroots_signing::request::SignRequest::authored_plan(&self) -> core::option::Option<&radroots_event_codec::authoring::AuthoredEventPlan> +pub const fn radroots_signing::request::SignRequest::authorization(&self) -> core::option::Option<radroots_signing::authorization::CurrentAuthoringDecision> +pub const fn radroots_signing::request::SignRequest::blossom_authorization_plan(&self) -> core::option::Option<&radroots_event_codec::authoring::BlossomAuthorizationPlan> +pub fn radroots_signing::request::SignRequest::blossom_upload(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::BlossomAuthorizationPlan, radroots_signing::request::SignPolicy) -> core::result::Result<Self, radroots_signing::error::Error> pub const fn radroots_signing::request::SignRequest::cancellation_signal(&self) -> &radroots_signing::request::CancellationSignal +pub fn radroots_signing::request::SignRequest::content(&self) -> &str +pub const fn radroots_signing::request::SignRequest::created_at(&self) -> u64 pub fn radroots_signing::request::SignRequest::ensure_active(&self, u64) -> core::result::Result<(), radroots_signing::error::Error> +pub const fn radroots_signing::request::SignRequest::expected_author(&self) -> &radroots_identity::key::PublicKey +pub const fn radroots_signing::request::SignRequest::expected_event_id(&self) -> &radroots_event::id::EventId pub const fn radroots_signing::request::SignRequest::intent_id(&self) -> radroots_signing::identity::SigningIntentId +pub const fn radroots_signing::request::SignRequest::kind(&self) -> u32 pub fn radroots_signing::request::SignRequest::new(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::AuthoredEventPlan, radroots_signing::request::SignPolicy) -> core::result::Result<Self, radroots_signing::error::Error> pub fn radroots_signing::request::SignRequest::new_with_authority(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::AuthoredEventPlan, radroots_signing::request::SignPolicy, &dyn radroots_signing::authorization::CurrentAuthoringAuthority) -> core::result::Result<Self, radroots_signing::error::Error> pub const fn radroots_signing::request::SignRequest::operation_kind(&self) -> radroots_protocol::runtime::v1::OperationId -pub const fn radroots_signing::request::SignRequest::plan(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_signing::request::SignRequest::plan_digest(&self) -> radroots_event_codec::authoring::PlanDigest pub const fn radroots_signing::request::SignRequest::policy(&self) -> radroots_signing::request::SignPolicy +pub const fn radroots_signing::request::SignRequest::purpose(&self) -> radroots_signing::request::SigningPurpose pub fn radroots_signing::request::SignRequest::report_progress(&self, &radroots_signing::status::SignProgress) pub const fn radroots_signing::request::SignRequest::signer_request_id(&self) -> radroots_signing::identity::SignerRequestId +pub fn radroots_signing::request::SignRequest::tags(&self) -> &[alloc::vec::Vec<alloc::string::String>] pub fn radroots_signing::request::SignRequest::with_cancellation_signal(self, radroots_signing::request::CancellationSignal) -> Self pub fn radroots_signing::request::SignRequest::with_progress_observer(self, alloc::sync::Arc<dyn radroots_signing::request::ProgressObserver>) -> Self impl core::fmt::Debug for radroots_signing::request::SignRequest @@ -259,6 +272,9 @@ pub radroots_signing::CurrentAuthoringDecision::Blocked pub radroots_signing::CurrentAuthoringDecision::Blocked::code: &'static str pub radroots_signing::CurrentAuthoringDecision::Revoked pub radroots_signing::CurrentAuthoringDecision::Revoked::code: &'static str +#[non_exhaustive] pub enum radroots_signing::SigningPurpose +pub radroots_signing::SigningPurpose::AuthoredEvent +pub radroots_signing::SigningPurpose::BlossomUploadAuthorization pub struct radroots_signing::Actor impl radroots_signing::actor::Actor pub const fn radroots_signing::actor::Actor::account_id(&self) -> core::option::Option<radroots_identity::account::AccountId> @@ -309,17 +325,27 @@ pub fn radroots_signing::receipt::SignReceipt::fmt(&self, &mut core::fmt::Format pub struct radroots_signing::SignRequest impl radroots_signing::request::SignRequest pub const fn radroots_signing::request::SignRequest::actor(&self) -> &radroots_signing::actor::Actor -pub const fn radroots_signing::request::SignRequest::authorization(&self) -> radroots_signing::authorization::CurrentAuthoringDecision +pub const fn radroots_signing::request::SignRequest::authored_plan(&self) -> core::option::Option<&radroots_event_codec::authoring::AuthoredEventPlan> +pub const fn radroots_signing::request::SignRequest::authorization(&self) -> core::option::Option<radroots_signing::authorization::CurrentAuthoringDecision> +pub const fn radroots_signing::request::SignRequest::blossom_authorization_plan(&self) -> core::option::Option<&radroots_event_codec::authoring::BlossomAuthorizationPlan> +pub fn radroots_signing::request::SignRequest::blossom_upload(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::BlossomAuthorizationPlan, radroots_signing::request::SignPolicy) -> core::result::Result<Self, radroots_signing::error::Error> pub const fn radroots_signing::request::SignRequest::cancellation_signal(&self) -> &radroots_signing::request::CancellationSignal +pub fn radroots_signing::request::SignRequest::content(&self) -> &str +pub const fn radroots_signing::request::SignRequest::created_at(&self) -> u64 pub fn radroots_signing::request::SignRequest::ensure_active(&self, u64) -> core::result::Result<(), radroots_signing::error::Error> +pub const fn radroots_signing::request::SignRequest::expected_author(&self) -> &radroots_identity::key::PublicKey +pub const fn radroots_signing::request::SignRequest::expected_event_id(&self) -> &radroots_event::id::EventId pub const fn radroots_signing::request::SignRequest::intent_id(&self) -> radroots_signing::identity::SigningIntentId +pub const fn radroots_signing::request::SignRequest::kind(&self) -> u32 pub fn radroots_signing::request::SignRequest::new(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::AuthoredEventPlan, radroots_signing::request::SignPolicy) -> core::result::Result<Self, radroots_signing::error::Error> pub fn radroots_signing::request::SignRequest::new_with_authority(radroots_protocol::runtime::v1::OperationId, radroots_signing::identity::SigningIntentId, radroots_signing::actor::Actor, radroots_event_codec::authoring::AuthoredEventPlan, radroots_signing::request::SignPolicy, &dyn radroots_signing::authorization::CurrentAuthoringAuthority) -> core::result::Result<Self, radroots_signing::error::Error> pub const fn radroots_signing::request::SignRequest::operation_kind(&self) -> radroots_protocol::runtime::v1::OperationId -pub const fn radroots_signing::request::SignRequest::plan(&self) -> &radroots_event_codec::authoring::AuthoredEventPlan +pub const fn radroots_signing::request::SignRequest::plan_digest(&self) -> radroots_event_codec::authoring::PlanDigest pub const fn radroots_signing::request::SignRequest::policy(&self) -> radroots_signing::request::SignPolicy +pub const fn radroots_signing::request::SignRequest::purpose(&self) -> radroots_signing::request::SigningPurpose pub fn radroots_signing::request::SignRequest::report_progress(&self, &radroots_signing::status::SignProgress) pub const fn radroots_signing::request::SignRequest::signer_request_id(&self) -> radroots_signing::identity::SignerRequestId +pub fn radroots_signing::request::SignRequest::tags(&self) -> &[alloc::vec::Vec<alloc::string::String>] pub fn radroots_signing::request::SignRequest::with_cancellation_signal(self, radroots_signing::request::CancellationSignal) -> Self pub fn radroots_signing::request::SignRequest::with_progress_observer(self, alloc::sync::Arc<dyn radroots_signing::request::ProgressObserver>) -> Self impl core::fmt::Debug for radroots_signing::request::SignRequest