commit 510269bfb468fab87117686ae23ec1ad8d14732a
parent b21032732edde890590cb3b86f2a7aa7cb252a9a
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 12:29:52 +0000
sdk: quarantine superseded package surface
- Remove unreachable predecessor modules, adapters, tests, and support sources.
- Guard the exact reachable source, registered target, and dependency boundary.
- Record the full first-party consumer audit and ordered CLI cutover steps.
- Keep the sole runtime-contract bridge retired, private, and outside the SDK.
Diffstat:
31 files changed, 153 insertions(+), 8213 deletions(-)
diff --git a/crates/sdk/src/actor_json.rs b/crates/sdk/src/actor_json.rs
@@ -1,62 +0,0 @@
-use radroots_event::contract::AuthorRole;
-use radroots_signing::{Actor, actor::ActorSource};
-use serde::{Serialize, ser::SerializeStruct};
-
-pub(crate) struct SdkActorContextJson<'a>(pub(crate) &'a Actor);
-
-pub(crate) fn serialize_actor_context<S>(actor: &Actor, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: serde::Serializer,
-{
- SdkActorContextJson(actor).serialize(serializer)
-}
-
-impl serde::Serialize for SdkActorContextJson<'_> {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let roles = self
- .0
- .roles()
- .iter()
- .map(actor_role_code)
- .collect::<Vec<_>>();
- let account_id = self.0.account_id().map(|account_id| account_id.to_hex());
- let pubkey = self.0.public_key().to_hex();
- let mut state = serializer.serialize_struct("SdkActorContext", 4)?;
- state.serialize_field("pubkey", &pubkey)?;
- state.serialize_field("roles", &roles)?;
- state.serialize_field("account_id", &account_id)?;
- state.serialize_field("source", actor_source_code(self.0.source()))?;
- state.end()
- }
-}
-
-fn actor_role_code(role: &AuthorRole) -> &'static str {
- match role {
- AuthorRole::Any => "any",
- AuthorRole::Application => "application",
- AuthorRole::Buyer => "buyer",
- AuthorRole::Farmer => "farmer",
- AuthorRole::Member => "member",
- AuthorRole::Moderator => "moderator",
- AuthorRole::Relay => "relay",
- AuthorRole::Seller => "seller",
- AuthorRole::Service => "service",
- }
-}
-
-fn actor_source_code(source: ActorSource) -> &'static str {
- match source {
- ActorSource::LocalAccount(_) => "local_account",
- ActorSource::ExplicitPublicKey => "explicit_public_key",
- ActorSource::RemoteSigner(_) => "remote_signer",
- ActorSource::Service(_) => "service",
- _ => "unknown",
- }
-}
-
-#[cfg(test)]
-#[path = "../tests/unit/actor_json_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/adapters/nostr.rs b/crates/sdk/src/adapters/nostr.rs
@@ -1,73 +0,0 @@
-use core::time::Duration;
-
-#[cfg(test)]
-use nostr::Keys as RadrootsNostrKeys;
-use radroots_nostr::event::{Event as RadrootsNostrEvent, EventId as RadrootsNostrEventId};
-#[cfg(test)]
-use radroots_transport_nostr::RadrootsNostrClientKey;
-use radroots_transport_nostr::{
- RadrootsNostrClient, RadrootsNostrClientOptions, RadrootsNostrOutput,
- RadrootsRelayTransportError,
-};
-
-pub fn signerless_client() -> RadrootsNostrClient {
- RadrootsNostrClient::new_signerless()
-}
-
-pub fn signerless_client_with_options(options: RadrootsNostrClientOptions) -> RadrootsNostrClient {
- RadrootsNostrClient::new_signerless_with_options(options)
-}
-
-#[cfg(test)]
-pub(crate) fn client_from_keys(keys: RadrootsNostrKeys) -> RadrootsNostrClient {
- let key = RadrootsNostrClientKey::from_secret_key_bytes(keys.secret_key().to_secret_bytes())
- .expect("an existing Nostr key remains valid at the transport boundary");
- RadrootsNostrClient::new(key)
-}
-
-pub async fn configure_write_relays(
- client: &RadrootsNostrClient,
- relay_urls: &[String],
- connect_timeout: Duration,
-) -> Result<(), RadrootsRelayTransportError> {
- for relay_url in relay_urls {
- client.add_write_relay(relay_url).await?;
- }
- client.connect().await;
- client.wait_for_connection(connect_timeout).await;
- Ok(())
-}
-
-#[cfg(test)]
-pub(crate) async fn connected_client_from_keys(
- keys: RadrootsNostrKeys,
- relay_urls: &[String],
- connect_timeout: Duration,
-) -> Result<RadrootsNostrClient, RadrootsRelayTransportError> {
- let client = client_from_keys(keys);
- configure_write_relays(&client, relay_urls, connect_timeout).await?;
- Ok(client)
-}
-
-pub async fn connected_relay_urls(client: &RadrootsNostrClient) -> Vec<String> {
- let mut relay_urls = client
- .relays()
- .await
- .into_values()
- .filter(|relay| relay.is_connected())
- .map(|relay| relay.url().to_string())
- .collect::<Vec<_>>();
- relay_urls.sort();
- relay_urls
-}
-
-pub async fn publish_signed_event(
- client: &RadrootsNostrClient,
- event: &RadrootsNostrEvent,
-) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsRelayTransportError> {
- client.send_event(event).await
-}
-
-#[cfg(test)]
-#[path = "../../tests/unit/adapters_nostr_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/adapters/signer.rs b/crates/sdk/src/adapters/signer.rs
@@ -1,24 +0,0 @@
-pub use radroots_nostr_connect::{
- BunkerUri, ClientUri, Error, Method, Permission, Request, Response,
- message::{
- PENDING_CONNECTION_ERROR, PendingConnectionOutcome, RPC_KIND, RemoteSessionCapability,
- RequestMessage, ResponseEnvelope,
- },
- permission::Permissions,
- uri::{ClientMetadata, Uri},
-};
-pub use radroots_nostr_signer::prelude::{
- RadrootsNostrEmbeddedSignerBackend, RadrootsNostrLocalSignerAvailability,
- RadrootsNostrLocalSignerCapability, RadrootsNostrRemoteSessionSignerCapability,
- RadrootsNostrSignerBackend, RadrootsNostrSignerBackendCapabilities,
- RadrootsNostrSignerCapability, RadrootsNostrSignerConnectEvaluation,
- RadrootsNostrSignerConnectProposal, RadrootsNostrSignerError,
- RadrootsNostrSignerHandledRequest, RadrootsNostrSignerHandledRequestOutcome,
- RadrootsNostrSignerManager, RadrootsNostrSignerNip46Codec,
- RadrootsNostrSignerNip46ConnectDecision, RadrootsNostrSignerNip46Handler,
- RadrootsNostrSignerNip46Policy, RadrootsNostrSignerNip46Signer,
- RadrootsNostrSignerPublishTransition, RadrootsNostrSignerRequestAction,
- RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerRequestResponseHint,
- RadrootsNostrSignerSessionLookup, connect_response_outcome, handled_request_for_action,
- response_from_hint,
-};
diff --git a/crates/sdk/src/geonames.rs b/crates/sdk/src/geonames.rs
@@ -1,142 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use std::path::{Path, PathBuf};
-
-use crate::{GeoNamesClient, RadrootsSdkError};
-pub use radroots_geocoder::{
- GEONAMES_1_0_ASSET, GEONAMES_ASSET_BYTE_SIZE, GEONAMES_ASSET_FILE_NAME, GEONAMES_ASSET_HOST,
- GEONAMES_ASSET_SHA256, GEONAMES_ASSET_URL, GEONAMES_ASSET_VERSION, GeoNamesAssetFetcher,
- GeoNamesAssetSpec, GeoNamesAssetState, GeoNamesAssetStatus, GeoNamesBlockingHttpFetcher,
- Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderLocalityCandidate,
- GeocoderLocalityInput, GeocoderLocalityLookup, GeocoderLocalityQuery, GeocoderPoint,
- GeocoderReverseOptions, GeocoderReverseResult, GeocoderStructuredLocalityQuery,
-};
-use radroots_geocoder::{
- ensure_default_geonames_asset_in_cache_root, ensure_geonames_asset_in_cache_root_with_fetcher,
- inspect_default_geonames_asset_in_cache_root, inspect_geonames_asset_path,
-};
-use radroots_runtime_paths::{
- default_shared_geonames_database_path_from_cache_root,
- default_shared_geonames_root_from_cache_root,
-};
-
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct RadrootsGeoNamesConfig {
- pub cache_root: PathBuf,
-}
-
-impl RadrootsGeoNamesConfig {
- pub fn new(cache_root: impl Into<PathBuf>) -> Self {
- Self {
- cache_root: cache_root.into(),
- }
- }
-
- pub fn root_path(&self) -> PathBuf {
- default_shared_geonames_root_from_cache_root(&self.cache_root)
- }
-
- pub fn database_path(&self) -> PathBuf {
- geonames_database_path_from_cache_root(&self.cache_root)
- }
-
- pub fn database_path_for_version(&self, version: &str) -> PathBuf {
- geonames_database_path_from_cache_root_for_version(&self.cache_root, version)
- }
-}
-
-impl<'sdk> GeoNamesClient<'sdk> {
- pub fn config(&self) -> Option<&RadrootsGeoNamesConfig> {
- self.sdk.geonames_config()
- }
-
- pub fn root_path(&self) -> Result<PathBuf, RadrootsSdkError> {
- Ok(self.required_config()?.root_path())
- }
-
- pub fn database_path(&self) -> Result<PathBuf, RadrootsSdkError> {
- Ok(self.required_config()?.database_path())
- }
-
- pub fn database_path_for_version(&self, version: &str) -> Result<PathBuf, RadrootsSdkError> {
- Ok(self.required_config()?.database_path_for_version(version))
- }
-
- pub fn inspect(&self) -> Result<GeoNamesAssetStatus, RadrootsSdkError> {
- inspect_default_geonames_asset_in_cache_root(&self.required_config()?.cache_root)
- .map_err(RadrootsSdkError::from)
- }
-
- pub fn inspect_path_with_spec(
- &self,
- path: impl AsRef<Path>,
- spec: &GeoNamesAssetSpec,
- ) -> Result<GeoNamesAssetStatus, RadrootsSdkError> {
- inspect_geonames_asset_path(path, spec).map_err(RadrootsSdkError::from)
- }
-
- pub fn ensure(&self) -> Result<GeoNamesAssetStatus, RadrootsSdkError> {
- ensure_default_geonames_asset_in_cache_root(&self.required_config()?.cache_root)
- .map_err(RadrootsSdkError::from)
- }
-
- pub fn ensure_with_fetcher<F>(
- &self,
- fetcher: &F,
- ) -> Result<GeoNamesAssetStatus, RadrootsSdkError>
- where
- F: GeoNamesAssetFetcher,
- {
- ensure_geonames_asset_in_cache_root_with_fetcher(
- &self.required_config()?.cache_root,
- &GEONAMES_1_0_ASSET,
- fetcher,
- )
- .map_err(RadrootsSdkError::from)
- }
-
- pub fn ensure_with_spec_and_fetcher<F>(
- &self,
- spec: &GeoNamesAssetSpec,
- fetcher: &F,
- ) -> Result<GeoNamesAssetStatus, RadrootsSdkError>
- where
- F: GeoNamesAssetFetcher,
- {
- ensure_geonames_asset_in_cache_root_with_fetcher(
- &self.required_config()?.cache_root,
- spec,
- fetcher,
- )
- .map_err(RadrootsSdkError::from)
- }
-
- pub fn open_verified(&self) -> Result<Geocoder, RadrootsSdkError> {
- Geocoder::open_verified_geonames_asset(self.database_path()?, &GEONAMES_1_0_ASSET)
- .map_err(RadrootsSdkError::from)
- }
-
- pub fn open_verified_path_with_spec(
- &self,
- path: impl AsRef<Path>,
- spec: &GeoNamesAssetSpec,
- ) -> Result<Geocoder, RadrootsSdkError> {
- Geocoder::open_verified_geonames_asset(path, spec).map_err(RadrootsSdkError::from)
- }
-
- fn required_config(&self) -> Result<&RadrootsGeoNamesConfig, RadrootsSdkError> {
- self.config()
- .ok_or_else(RadrootsSdkError::missing_geonames_config)
- }
-}
-
-fn geonames_database_path_from_cache_root(cache_root: impl AsRef<Path>) -> PathBuf {
- default_shared_geonames_database_path_from_cache_root(cache_root, GEONAMES_ASSET_VERSION)
-}
-
-fn geonames_database_path_from_cache_root_for_version(
- cache_root: impl AsRef<Path>,
- version: &str,
-) -> PathBuf {
- default_shared_geonames_database_path_from_cache_root(cache_root, version)
-}
diff --git a/crates/sdk/src/idempotency.rs b/crates/sdk/src/idempotency.rs
@@ -1,118 +0,0 @@
-use crate::RadrootsSdkError;
-use core::fmt;
-use radroots_event::id::EventId;
-use radroots_identity::PublicKey;
-use serde::ser::SerializeStruct;
-
-pub const SDK_IDEMPOTENCY_KEY_MAX_LEN: usize = 256;
-
-#[derive(Clone, PartialEq, Eq, Hash)]
-pub struct SdkIdempotencyKey(String);
-
-impl SdkIdempotencyKey {
- pub fn new(value: impl AsRef<str>) -> Result<Self, RadrootsSdkError> {
- let value = value.as_ref();
- if value.is_empty() {
- return Err(invalid_request("idempotency key must not be empty"));
- }
- if value.trim() != value {
- return Err(invalid_request(
- "idempotency key must not include boundary whitespace",
- ));
- }
- if value.len() > SDK_IDEMPOTENCY_KEY_MAX_LEN {
- return Err(invalid_request(format!(
- "idempotency key must be at most {SDK_IDEMPOTENCY_KEY_MAX_LEN} bytes"
- )));
- }
- if value.chars().any(char::is_control) {
- return Err(invalid_request(
- "idempotency key must not contain control characters",
- ));
- }
- if !is_uuid_v7(value) {
- return Err(invalid_request("idempotency key must be a UUIDv7"));
- }
- Ok(Self(value.to_owned()))
- }
-
- pub fn as_str(&self) -> &str {
- self.0.as_str()
- }
-
- pub fn into_string(self) -> String {
- self.0
- }
-}
-
-impl fmt::Debug for SdkIdempotencyKey {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("SdkIdempotencyKey")
- .field("value", &"<redacted>")
- .field("len", &self.0.len())
- .finish()
- }
-}
-
-impl serde::Serialize for SdkIdempotencyKey {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let mut state = serializer.serialize_struct("SdkIdempotencyKey", 2)?;
- state.serialize_field("value", "<redacted>")?;
- state.serialize_field("len", &self.0.len())?;
- state.end()
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct SdkTradeIdempotencyRecord {
- pub idempotency_key: SdkIdempotencyKey,
- pub operation_kind: String,
- pub actor_pubkey: PublicKey,
- pub digest: String,
- pub canonical_payload_hash: String,
- pub expected_event_id: EventId,
- pub outbox_operation_id: i64,
-}
-
-impl SdkTradeIdempotencyRecord {
- pub fn matches_payload(&self, canonical_payload_hash: &str) -> bool {
- self.canonical_payload_hash == canonical_payload_hash
- }
-
- pub fn conflict_error(&self, new_digest: impl Into<String>) -> RadrootsSdkError {
- RadrootsSdkError::IdempotencyConflict {
- operation_kind: self.operation_kind.clone(),
- expected_pubkey_prefix: self.actor_pubkey.to_hex().chars().take(12).collect(),
- existing_digest_prefix: self.digest.chars().take(12).collect(),
- new_digest_prefix: new_digest.into().chars().take(12).collect(),
- }
- }
-}
-
-fn invalid_request(message: impl Into<String>) -> RadrootsSdkError {
- RadrootsSdkError::InvalidRequest {
- message: message.into(),
- }
-}
-
-fn is_uuid_v7(value: &str) -> bool {
- let bytes = value.as_bytes();
- bytes.len() == 36
- && bytes[8] == b'-'
- && bytes[13] == b'-'
- && bytes[18] == b'-'
- && bytes[23] == b'-'
- && bytes[14] == b'7'
- && matches!(bytes[19], b'8' | b'9' | b'a' | b'b')
- && bytes
- .iter()
- .enumerate()
- .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
-}
-
-#[cfg(test)]
-#[path = "../tests/unit/idempotency_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/identity.rs b/crates/sdk/src/identity.rs
@@ -1,4 +0,0 @@
-pub use radroots_identity::{
- AccountId, Error, IdentityId, Profile, PublicIdentity, PublicKey, Username, account, key,
- profile, username,
-};
diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs
@@ -1,1324 +0,0 @@
-#[cfg(not(feature = "std"))]
-use alloc::{
- string::{String, ToString},
- vec::Vec,
-};
-#[cfg(feature = "std")]
-use std::{
- string::{String, ToString},
- vec::Vec,
-};
-
-use core::fmt;
-
-pub use radroots_event::wire::Nip01EventWireParts;
-pub use radroots_event::{
- draft::{DraftError, EventDraft},
- envelope::EventEnvelope,
- envelope::kind::{
- KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT,
- KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE,
- KIND_WIKI_MERGE_REQUEST, KIND_WIKI_REDIRECT,
- },
- knowledge::{
- AddressableRef, ContributionAttestation, EvidenceBounty, KnowledgeChangeProposal,
- KnowledgeCitationSpan, KnowledgeClaim, KnowledgeFieldContext, KnowledgeFieldReport,
- KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation,
- KnowledgeObservationValue, KnowledgeRelation, KnowledgeReview, KnowledgeReviewScope,
- KnowledgeReviewScore, KnowledgeReviewTarget, KnowledgeSource, KnowledgeValidationError,
- RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA,
- RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA,
- RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
- RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RADROOTS_WIKI_D_TAG_MAX_LEN, RightsAssertion,
- WikiArticle, WikiArticleVersionRef, WikiDTagError, WikiMergeRequest, WikiRedirect,
- normalize_wiki_d_tag, validate_knowledge_claim, validate_wiki_article, validate_wiki_d_tag,
- },
- tag::EventRef,
-};
-pub use radroots_event_codec::{
- encode::RadrootsEncodeError,
- manifest::{
- RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, RadrootsKnowledgeContractManifest,
- RadrootsKnowledgeContractManifestEntry, RadrootsKnowledgeManifestCodecSupport,
- RadrootsKnowledgeManifestDiscriminator, RadrootsKnowledgeManifestTagContract,
- },
- verify::{
- RadrootsContractValidatedEvent, RadrootsDecodeError, RadrootsDecodedEvent,
- RadrootsIdVerifiedEvent, RadrootsNip01VerificationError, RadrootsSignatureVerifiedEvent,
- },
-};
-
-use radroots_event::knowledge::{
- validate_knowledge_field_report, validate_knowledge_relation, validate_knowledge_review,
- validate_knowledge_source, validate_wiki_merge_request, validate_wiki_redirect,
-};
-use radroots_event_codec::{
- encode::knowledge::{
- knowledge_claim_to_wire_parts, knowledge_field_report_to_wire_parts,
- knowledge_relation_to_wire_parts, knowledge_review_to_wire_parts,
- knowledge_source_to_wire_parts, wiki_article_to_wire_parts,
- wiki_merge_request_to_wire_parts, wiki_redirect_to_wire_parts,
- },
- manifest::{
- contract_manifest_json as codec_contract_manifest_json,
- contract_manifest_sha256 as codec_contract_manifest_sha256, knowledge_contract_manifest,
- },
- verify::verify_and_decode_radroots_event as codec_verify_and_decode,
-};
-
-pub const WIKI_ARTICLE_CONTRACT_ID: &str = "radroots.wiki.article.v1";
-pub const WIKI_REDIRECT_CONTRACT_ID: &str = "radroots.wiki.redirect.v1";
-pub const WIKI_MERGE_REQUEST_CONTRACT_ID: &str = "radroots.wiki.merge_request.v1";
-pub const KNOWLEDGE_SOURCE_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_SOURCE_SCHEMA;
-pub const KNOWLEDGE_CLAIM_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_CLAIM_SCHEMA;
-pub const KNOWLEDGE_RELATION_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_RELATION_SCHEMA;
-pub const KNOWLEDGE_REVIEW_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_REVIEW_SCHEMA;
-pub const KNOWLEDGE_FIELD_REPORT_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA;
-
-#[derive(Debug)]
-pub enum RadrootsSdkKnowledgeError {
- Builder(RadrootsKnowledgeBuilderError),
- Encode(RadrootsEncodeError),
- Draft(DraftError),
- Decode(RadrootsDecodeError),
- Manifest(serde_json::Error),
-}
-
-impl RadrootsSdkKnowledgeError {
- pub const fn code(&self) -> &'static str {
- match self {
- Self::Builder(_) => "knowledge_builder",
- Self::Encode(_) => "knowledge_encode",
- Self::Draft(_) => "knowledge_draft",
- Self::Decode(_) => "knowledge_decode",
- Self::Manifest(_) => "knowledge_manifest",
- }
- }
-
- pub fn inner_code(&self) -> &'static str {
- match self {
- Self::Builder(error) => error.code(),
- Self::Encode(error) => error.code(),
- Self::Draft(error) => draft_error_code(error),
- Self::Decode(error) => error.code(),
- Self::Manifest(_) => "json",
- }
- }
-}
-
-impl fmt::Display for RadrootsSdkKnowledgeError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Builder(_) => formatter.write_str("knowledge event builder failed"),
- Self::Encode(_) => formatter.write_str("knowledge event encoding failed"),
- Self::Draft(_) => formatter.write_str("knowledge event draft preparation failed"),
- Self::Decode(_) => formatter.write_str("knowledge event verification or decode failed"),
- Self::Manifest(_) => {
- formatter.write_str("knowledge contract manifest rendering failed")
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for RadrootsSdkKnowledgeError {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Builder(error) => Some(error),
- Self::Encode(error) => Some(error),
- Self::Draft(error) => Some(error),
- Self::Decode(error) => Some(error),
- Self::Manifest(error) => Some(error),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum RadrootsKnowledgeBuilderError {
- MissingField(&'static str),
- InvalidField(&'static str),
-}
-
-impl RadrootsKnowledgeBuilderError {
- pub const fn code(&self) -> &'static str {
- match self {
- Self::MissingField(_) => "missing_field",
- Self::InvalidField(_) => "invalid_field",
- }
- }
-
- pub const fn field(&self) -> &'static str {
- match self {
- Self::MissingField(field) | Self::InvalidField(field) => field,
- }
- }
-}
-
-impl fmt::Display for RadrootsKnowledgeBuilderError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::MissingField(field) => write!(
- formatter,
- "knowledge builder missing required field {field}"
- ),
- Self::InvalidField(field) => {
- write!(formatter, "knowledge builder invalid field {field}")
- }
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for RadrootsKnowledgeBuilderError {}
-
-impl From<RadrootsKnowledgeBuilderError> for RadrootsSdkKnowledgeError {
- fn from(value: RadrootsKnowledgeBuilderError) -> Self {
- Self::Builder(value)
- }
-}
-
-impl From<RadrootsEncodeError> for RadrootsSdkKnowledgeError {
- fn from(value: RadrootsEncodeError) -> Self {
- Self::Encode(value)
- }
-}
-
-impl From<DraftError> for RadrootsSdkKnowledgeError {
- fn from(value: DraftError) -> Self {
- Self::Draft(value)
- }
-}
-
-impl From<RadrootsDecodeError> for RadrootsSdkKnowledgeError {
- fn from(value: RadrootsDecodeError) -> Self {
- Self::Decode(value)
- }
-}
-
-impl From<serde_json::Error> for RadrootsSdkKnowledgeError {
- fn from(value: serde_json::Error) -> Self {
- Self::Manifest(value)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsWikiArticleBuilder {
- d_tag: String,
- title: Option<String>,
- content_djot: Option<String>,
- summary: Option<String>,
- topics: Vec<String>,
- references: Vec<EventRef>,
- forked_from: Vec<WikiArticleVersionRef>,
- deferred_to: Option<WikiArticleVersionRef>,
-}
-
-impl RadrootsWikiArticleBuilder {
- pub fn new(d_tag: impl Into<String>) -> Self {
- Self {
- d_tag: d_tag.into(),
- ..Self::default()
- }
- }
-
- pub fn title(mut self, title: impl Into<String>) -> Self {
- self.title = Some(title.into());
- self
- }
-
- pub fn content_djot(mut self, content_djot: impl Into<String>) -> Self {
- self.content_djot = Some(content_djot.into());
- self
- }
-
- pub fn summary(mut self, summary: impl Into<String>) -> Self {
- self.summary = Some(summary.into());
- self
- }
-
- pub fn topic(mut self, topic: impl Into<String>) -> Self {
- self.topics.push(topic.into());
- self
- }
-
- pub fn reference(mut self, reference: EventRef) -> Self {
- self.references.push(reference);
- self
- }
-
- pub fn forked_from(mut self, version_ref: WikiArticleVersionRef) -> Self {
- self.forked_from.push(version_ref);
- self
- }
-
- pub fn deferred_to(mut self, version_ref: WikiArticleVersionRef) -> Self {
- self.deferred_to = Some(version_ref);
- self
- }
-
- pub fn build(self) -> Result<WikiArticle, RadrootsKnowledgeBuilderError> {
- let article = WikiArticle {
- d_tag: self.d_tag,
- title: self.title,
- content_djot: builder_required_string(self.content_djot, "content_djot")?,
- summary: self.summary,
- topics: self.topics,
- references: self.references,
- forked_from: self.forked_from,
- deferred_to: self.deferred_to,
- };
- builder_validated(article, validate_wiki_article)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_article_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_article_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsWikiRedirectBuilder {
- d_tag: String,
- target: Option<AddressableRef>,
-}
-
-impl RadrootsWikiRedirectBuilder {
- pub fn new(d_tag: impl Into<String>) -> Self {
- Self {
- d_tag: d_tag.into(),
- target: None,
- }
- }
-
- pub fn target(mut self, target: AddressableRef) -> Self {
- self.target = Some(target);
- self
- }
-
- pub fn build(self) -> Result<WikiRedirect, RadrootsKnowledgeBuilderError> {
- let redirect = WikiRedirect {
- d_tag: self.d_tag,
- target: builder_required(self.target, "target")?,
- };
- builder_validated(redirect, validate_wiki_redirect)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_redirect_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_redirect_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsWikiMergeRequestBuilder {
- target_article: Option<AddressableRef>,
- destination_pubkey: Option<String>,
- base_version_event_id: Option<String>,
- source_version_event_id: Option<String>,
- explanation: Option<String>,
-}
-
-impl RadrootsWikiMergeRequestBuilder {
- pub fn new() -> Self {
- Self {
- target_article: None,
- destination_pubkey: None,
- base_version_event_id: None,
- source_version_event_id: None,
- explanation: None,
- }
- }
-
- pub fn target_article(mut self, target_article: AddressableRef) -> Self {
- self.target_article = Some(target_article);
- self
- }
-
- pub fn destination_pubkey(mut self, destination_pubkey: impl Into<String>) -> Self {
- self.destination_pubkey = Some(destination_pubkey.into());
- self
- }
-
- pub fn base_version_event_id(mut self, base_version_event_id: impl Into<String>) -> Self {
- self.base_version_event_id = Some(base_version_event_id.into());
- self
- }
-
- pub fn source_version_event_id(mut self, source_version_event_id: impl Into<String>) -> Self {
- self.source_version_event_id = Some(source_version_event_id.into());
- self
- }
-
- pub fn explanation(mut self, explanation: impl Into<String>) -> Self {
- self.explanation = Some(explanation.into());
- self
- }
-
- pub fn build(self) -> Result<WikiMergeRequest, RadrootsKnowledgeBuilderError> {
- let request = WikiMergeRequest {
- target_article: builder_required(self.target_article, "target_article")?,
- destination_pubkey: builder_required_string(
- self.destination_pubkey,
- "destination_pubkey",
- )?,
- base_version_event_id: self.base_version_event_id,
- source_version_event_id: builder_required_string(
- self.source_version_event_id,
- "source_version_event_id",
- )?,
- explanation: self.explanation,
- };
- builder_validated(request, validate_wiki_merge_request)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_merge_request_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_merge_request_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsKnowledgeSourceBuilder {
- d_tag: String,
- title: Option<String>,
- source_type: Option<String>,
- authors: Vec<String>,
- publisher: Option<String>,
- publication_year: Option<u16>,
- edition: Option<String>,
- canonical_url: Option<String>,
- artifact_refs: Vec<EventRef>,
- author_asserted_rights: Option<RightsAssertion>,
- topics: Vec<String>,
- summary: Option<String>,
-}
-
-impl RadrootsKnowledgeSourceBuilder {
- pub fn new(d_tag: impl Into<String>) -> Self {
- Self {
- d_tag: d_tag.into(),
- ..Self::default()
- }
- }
-
- pub fn title(mut self, title: impl Into<String>) -> Self {
- self.title = Some(title.into());
- self
- }
-
- pub fn source_type(mut self, source_type: impl Into<String>) -> Self {
- self.source_type = Some(source_type.into());
- self
- }
-
- pub fn author(mut self, author: impl Into<String>) -> Self {
- self.authors.push(author.into());
- self
- }
-
- pub fn publisher(mut self, publisher: impl Into<String>) -> Self {
- self.publisher = Some(publisher.into());
- self
- }
-
- pub fn publication_year(mut self, publication_year: u16) -> Self {
- self.publication_year = Some(publication_year);
- self
- }
-
- pub fn edition(mut self, edition: impl Into<String>) -> Self {
- self.edition = Some(edition.into());
- self
- }
-
- pub fn canonical_url(mut self, canonical_url: impl Into<String>) -> Self {
- self.canonical_url = Some(canonical_url.into());
- self
- }
-
- pub fn artifact_ref(mut self, artifact_ref: EventRef) -> Self {
- self.artifact_refs.push(artifact_ref);
- self
- }
-
- pub fn author_asserted_rights(mut self, rights: RightsAssertion) -> Self {
- self.author_asserted_rights = Some(rights);
- self
- }
-
- pub fn topic(mut self, topic: impl Into<String>) -> Self {
- self.topics.push(topic.into());
- self
- }
-
- pub fn summary(mut self, summary: impl Into<String>) -> Self {
- self.summary = Some(summary.into());
- self
- }
-
- pub fn build(self) -> Result<KnowledgeSource, RadrootsKnowledgeBuilderError> {
- let source = KnowledgeSource {
- schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- d_tag: self.d_tag,
- title: builder_required_string(self.title, "title")?,
- source_type: builder_required_string(self.source_type, "source_type")?,
- authors: self.authors,
- publisher: self.publisher,
- publication_year: self.publication_year,
- edition: self.edition,
- canonical_url: self.canonical_url,
- artifact_refs: self.artifact_refs,
- author_asserted_rights: self.author_asserted_rights,
- topics: self.topics,
- summary: self.summary,
- };
- builder_validated(source, validate_knowledge_source)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_source_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_source_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsKnowledgeClaimBuilder {
- claim_type: Option<String>,
- text: Option<String>,
- citation_spans: Vec<KnowledgeCitationSpan>,
- topics: Vec<String>,
- applies_to: Vec<String>,
- author_asserted_confidence: Option<String>,
- supersedes: Vec<EventRef>,
-}
-
-impl RadrootsKnowledgeClaimBuilder {
- pub fn new() -> Self {
- Self {
- claim_type: None,
- text: None,
- citation_spans: Vec::new(),
- topics: Vec::new(),
- applies_to: Vec::new(),
- author_asserted_confidence: None,
- supersedes: Vec::new(),
- }
- }
-
- pub fn claim_type(mut self, claim_type: impl Into<String>) -> Self {
- self.claim_type = Some(claim_type.into());
- self
- }
-
- pub fn text(mut self, text: impl Into<String>) -> Self {
- self.text = Some(text.into());
- self
- }
-
- pub fn citation_span(mut self, citation_span: KnowledgeCitationSpan) -> Self {
- self.citation_spans.push(citation_span);
- self
- }
-
- pub fn topic(mut self, topic: impl Into<String>) -> Self {
- self.topics.push(topic.into());
- self
- }
-
- pub fn applies_to(mut self, applies_to: impl Into<String>) -> Self {
- self.applies_to.push(applies_to.into());
- self
- }
-
- pub fn author_asserted_confidence(
- mut self,
- author_asserted_confidence: impl Into<String>,
- ) -> Self {
- self.author_asserted_confidence = Some(author_asserted_confidence.into());
- self
- }
-
- pub fn supersedes(mut self, supersedes: EventRef) -> Self {
- self.supersedes.push(supersedes);
- self
- }
-
- pub fn build(self) -> Result<KnowledgeClaim, RadrootsKnowledgeBuilderError> {
- let claim = KnowledgeClaim {
- schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- claim_type: builder_required_string(self.claim_type, "claim_type")?,
- text: builder_required_string(self.text, "text")?,
- citation_spans: self.citation_spans,
- topics: self.topics,
- applies_to: self.applies_to,
- author_asserted_confidence: self.author_asserted_confidence,
- supersedes: self.supersedes,
- };
- builder_validated(claim, validate_knowledge_claim)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_claim_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_claim_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsKnowledgeRelationBuilder {
- subject: Option<KnowledgeNodeRef>,
- predicate: Option<String>,
- object: Option<KnowledgeNodeRef>,
- support_refs: Vec<EventRef>,
- author_asserted_confidence: Option<String>,
- supersedes: Vec<EventRef>,
-}
-
-impl RadrootsKnowledgeRelationBuilder {
- pub fn new() -> Self {
- Self {
- subject: None,
- predicate: None,
- object: None,
- support_refs: Vec::new(),
- author_asserted_confidence: None,
- supersedes: Vec::new(),
- }
- }
-
- pub fn subject(mut self, subject: KnowledgeNodeRef) -> Self {
- self.subject = Some(subject);
- self
- }
-
- pub fn predicate(mut self, predicate: impl Into<String>) -> Self {
- self.predicate = Some(predicate.into());
- self
- }
-
- pub fn object(mut self, object: KnowledgeNodeRef) -> Self {
- self.object = Some(object);
- self
- }
-
- pub fn support_ref(mut self, support_ref: EventRef) -> Self {
- self.support_refs.push(support_ref);
- self
- }
-
- pub fn author_asserted_confidence(
- mut self,
- author_asserted_confidence: impl Into<String>,
- ) -> Self {
- self.author_asserted_confidence = Some(author_asserted_confidence.into());
- self
- }
-
- pub fn supersedes(mut self, supersedes: EventRef) -> Self {
- self.supersedes.push(supersedes);
- self
- }
-
- pub fn build(self) -> Result<KnowledgeRelation, RadrootsKnowledgeBuilderError> {
- let relation = KnowledgeRelation {
- schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- subject: builder_required(self.subject, "subject")?,
- predicate: builder_required_string(self.predicate, "predicate")?,
- object: builder_required(self.object, "object")?,
- support_refs: self.support_refs,
- author_asserted_confidence: self.author_asserted_confidence,
- supersedes: self.supersedes,
- };
- builder_validated(relation, validate_knowledge_relation)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_relation_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_relation_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsKnowledgeReviewBuilder {
- target: Option<KnowledgeReviewTarget>,
- reviewer_role: Option<String>,
- verdict: Option<String>,
- scores: Vec<KnowledgeReviewScore>,
- notes: Option<String>,
- evidence_refs: Vec<EventRef>,
-}
-
-impl RadrootsKnowledgeReviewBuilder {
- pub fn new() -> Self {
- Self {
- target: None,
- reviewer_role: None,
- verdict: None,
- scores: Vec::new(),
- notes: None,
- evidence_refs: Vec::new(),
- }
- }
-
- pub fn target(mut self, target: KnowledgeReviewTarget) -> Self {
- self.target = Some(target);
- self
- }
-
- pub fn reviewer_role(mut self, reviewer_role: impl Into<String>) -> Self {
- self.reviewer_role = Some(reviewer_role.into());
- self
- }
-
- pub fn verdict(mut self, verdict: impl Into<String>) -> Self {
- self.verdict = Some(verdict.into());
- self
- }
-
- pub fn score(mut self, score: KnowledgeReviewScore) -> Self {
- self.scores.push(score);
- self
- }
-
- pub fn notes(mut self, notes: impl Into<String>) -> Self {
- self.notes = Some(notes.into());
- self
- }
-
- pub fn evidence_ref(mut self, evidence_ref: EventRef) -> Self {
- self.evidence_refs.push(evidence_ref);
- self
- }
-
- pub fn build(self) -> Result<KnowledgeReview, RadrootsKnowledgeBuilderError> {
- let review = KnowledgeReview {
- schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- target: builder_required(self.target, "target")?,
- reviewer_role: builder_required_string(self.reviewer_role, "reviewer_role")?,
- verdict: builder_required_string(self.verdict, "verdict")?,
- scores: self.scores,
- notes: self.notes,
- evidence_refs: self.evidence_refs,
- };
- builder_validated(review, validate_knowledge_review)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_review_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_review_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
-pub struct RadrootsKnowledgeFieldReportBuilder {
- report_type: Option<String>,
- title: Option<String>,
- summary: Option<String>,
- context: Option<KnowledgeFieldContext>,
- observations: Vec<KnowledgeObservation>,
- artifact_refs: Vec<EventRef>,
- related_refs: Vec<EventRef>,
- limitations: Vec<String>,
-}
-
-impl RadrootsKnowledgeFieldReportBuilder {
- pub fn new() -> Self {
- Self {
- report_type: None,
- title: None,
- summary: None,
- context: None,
- observations: Vec::new(),
- artifact_refs: Vec::new(),
- related_refs: Vec::new(),
- limitations: Vec::new(),
- }
- }
-
- pub fn report_type(mut self, report_type: impl Into<String>) -> Self {
- self.report_type = Some(report_type.into());
- self
- }
-
- pub fn title(mut self, title: impl Into<String>) -> Self {
- self.title = Some(title.into());
- self
- }
-
- pub fn summary(mut self, summary: impl Into<String>) -> Self {
- self.summary = Some(summary.into());
- self
- }
-
- pub fn context(mut self, context: KnowledgeFieldContext) -> Self {
- self.context = Some(context);
- self
- }
-
- pub fn observation(mut self, observation: KnowledgeObservation) -> Self {
- self.observations.push(observation);
- self
- }
-
- pub fn artifact_ref(mut self, artifact_ref: EventRef) -> Self {
- self.artifact_refs.push(artifact_ref);
- self
- }
-
- pub fn related_ref(mut self, related_ref: EventRef) -> Self {
- self.related_refs.push(related_ref);
- self
- }
-
- pub fn limitation(mut self, limitation: impl Into<String>) -> Self {
- self.limitations.push(limitation.into());
- self
- }
-
- pub fn build(self) -> Result<KnowledgeFieldReport, RadrootsKnowledgeBuilderError> {
- let report = KnowledgeFieldReport {
- schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- report_type: builder_required_string(self.report_type, "report_type")?,
- title: builder_required_string(self.title, "title")?,
- summary: self.summary,
- context: builder_required(self.context, "context")?,
- observations: self.observations,
- artifact_refs: self.artifact_refs,
- related_refs: self.related_refs,
- limitations: self.limitations,
- };
- builder_validated(report, validate_knowledge_field_report)
- }
-
- pub fn build_event(self) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_field_report_event(&self.build()?)
- }
-
- pub fn build_draft(
- self,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_field_report_draft(&self.build()?, expected_pubkey, created_at)
- }
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-pub struct KnowledgeEventBuilder;
-
-impl KnowledgeEventBuilder {
- pub const fn new() -> Self {
- Self
- }
-
- pub fn wiki_article(
- &self,
- article: &WikiArticle,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_article_event(article)
- }
-
- pub fn wiki_redirect(
- &self,
- redirect: &WikiRedirect,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_redirect_event(redirect)
- }
-
- pub fn wiki_merge_request(
- &self,
- request: &WikiMergeRequest,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_wiki_merge_request_event(request)
- }
-
- pub fn knowledge_source(
- &self,
- source: &KnowledgeSource,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_source_event(source)
- }
-
- pub fn knowledge_claim(
- &self,
- claim: &KnowledgeClaim,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_claim_event(claim)
- }
-
- pub fn knowledge_relation(
- &self,
- relation: &KnowledgeRelation,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_relation_event(relation)
- }
-
- pub fn knowledge_review(
- &self,
- review: &KnowledgeReview,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_review_event(review)
- }
-
- pub fn knowledge_field_report(
- &self,
- report: &KnowledgeFieldReport,
- ) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- build_knowledge_field_report_event(report)
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct KnowledgeDraftBuilder {
- expected_pubkey: String,
- created_at: u32,
-}
-
-impl KnowledgeDraftBuilder {
- pub fn new(expected_pubkey: impl AsRef<str>, created_at: u32) -> Self {
- Self {
- expected_pubkey: expected_pubkey.as_ref().to_string(),
- created_at,
- }
- }
-
- pub fn expected_pubkey(&self) -> &str {
- self.expected_pubkey.as_str()
- }
-
- pub const fn created_at(&self) -> u32 {
- self.created_at
- }
-
- pub fn wiki_article(
- &self,
- article: &WikiArticle,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_article_draft(article, self.expected_pubkey(), self.created_at)
- }
-
- pub fn wiki_redirect(
- &self,
- redirect: &WikiRedirect,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_redirect_draft(redirect, self.expected_pubkey(), self.created_at)
- }
-
- pub fn wiki_merge_request(
- &self,
- request: &WikiMergeRequest,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_wiki_merge_request_draft(request, self.expected_pubkey(), self.created_at)
- }
-
- pub fn knowledge_source(
- &self,
- source: &KnowledgeSource,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_source_draft(source, self.expected_pubkey(), self.created_at)
- }
-
- pub fn knowledge_claim(
- &self,
- claim: &KnowledgeClaim,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_claim_draft(claim, self.expected_pubkey(), self.created_at)
- }
-
- pub fn knowledge_relation(
- &self,
- relation: &KnowledgeRelation,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_relation_draft(relation, self.expected_pubkey(), self.created_at)
- }
-
- pub fn knowledge_review(
- &self,
- review: &KnowledgeReview,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_review_draft(review, self.expected_pubkey(), self.created_at)
- }
-
- pub fn knowledge_field_report(
- &self,
- report: &KnowledgeFieldReport,
- ) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_knowledge_field_report_draft(report, self.expected_pubkey(), self.created_at)
- }
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-pub struct KnowledgeCodec;
-
-impl KnowledgeCodec {
- pub const fn new() -> Self {
- Self
- }
-
- pub fn verify_and_decode_radroots_event(
- &self,
- event: EventEnvelope,
- ) -> Result<RadrootsDecodedEvent, RadrootsSdkKnowledgeError> {
- verify_and_decode_radroots_event(event)
- }
-
- pub fn contract_manifest(&self) -> RadrootsKnowledgeContractManifest {
- contract_manifest()
- }
-
- pub fn contract_manifest_json(&self) -> Result<String, RadrootsSdkKnowledgeError> {
- contract_manifest_json()
- }
-
- pub fn contract_manifest_sha256(&self) -> Result<String, RadrootsSdkKnowledgeError> {
- contract_manifest_sha256()
- }
-}
-
-pub fn build_wiki_article_event(
- article: &WikiArticle,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(wiki_article_to_wire_parts(article)?)
-}
-
-pub fn build_wiki_redirect_event(
- redirect: &WikiRedirect,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(wiki_redirect_to_wire_parts(redirect)?)
-}
-
-pub fn build_wiki_merge_request_event(
- request: &WikiMergeRequest,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(wiki_merge_request_to_wire_parts(request)?)
-}
-
-pub fn build_knowledge_source_event(
- source: &KnowledgeSource,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(knowledge_source_to_wire_parts(source)?)
-}
-
-pub fn build_knowledge_claim_event(
- claim: &KnowledgeClaim,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(knowledge_claim_to_wire_parts(claim)?)
-}
-
-pub fn build_knowledge_relation_event(
- relation: &KnowledgeRelation,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(knowledge_relation_to_wire_parts(relation)?)
-}
-
-pub fn build_knowledge_review_event(
- review: &KnowledgeReview,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(knowledge_review_to_wire_parts(review)?)
-}
-
-pub fn build_knowledge_field_report_event(
- report: &KnowledgeFieldReport,
-) -> Result<Nip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(knowledge_field_report_to_wire_parts(report)?)
-}
-
-pub fn prepare_wiki_article_draft(
- article: &WikiArticle,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_wiki_article_event(article)?,
- WIKI_ARTICLE_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_wiki_redirect_draft(
- redirect: &WikiRedirect,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_wiki_redirect_event(redirect)?,
- WIKI_REDIRECT_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_wiki_merge_request_draft(
- request: &WikiMergeRequest,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_wiki_merge_request_event(request)?,
- WIKI_MERGE_REQUEST_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_knowledge_source_draft(
- source: &KnowledgeSource,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_knowledge_source_event(source)?,
- KNOWLEDGE_SOURCE_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_knowledge_claim_draft(
- claim: &KnowledgeClaim,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_knowledge_claim_event(claim)?,
- KNOWLEDGE_CLAIM_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_knowledge_relation_draft(
- relation: &KnowledgeRelation,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_knowledge_relation_event(relation)?,
- KNOWLEDGE_RELATION_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_knowledge_review_draft(
- review: &KnowledgeReview,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_knowledge_review_event(review)?,
- KNOWLEDGE_REVIEW_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn prepare_knowledge_field_report_draft(
- report: &KnowledgeFieldReport,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- prepare_draft(
- build_knowledge_field_report_event(report)?,
- KNOWLEDGE_FIELD_REPORT_CONTRACT_ID,
- expected_pubkey,
- created_at,
- )
-}
-
-pub fn verify_and_decode_radroots_event(
- event: EventEnvelope,
-) -> Result<RadrootsDecodedEvent, RadrootsSdkKnowledgeError> {
- Ok(codec_verify_and_decode(event)?)
-}
-
-pub fn contract_manifest() -> RadrootsKnowledgeContractManifest {
- knowledge_contract_manifest()
-}
-
-pub fn contract_manifest_json() -> Result<String, RadrootsSdkKnowledgeError> {
- Ok(codec_contract_manifest_json()?)
-}
-
-pub fn contract_manifest_sha256() -> Result<String, RadrootsSdkKnowledgeError> {
- Ok(codec_contract_manifest_sha256()?)
-}
-
-fn prepare_draft(
- parts: Nip01EventWireParts,
- contract_id: &'static str,
- expected_pubkey: impl AsRef<str>,
- created_at: u32,
-) -> Result<EventDraft, RadrootsSdkKnowledgeError> {
- Ok(EventDraft::new(
- contract_id,
- parts.kind,
- u64::from(created_at),
- parts.tags,
- parts.content,
- expected_pubkey.as_ref(),
- )?)
-}
-
-fn draft_error_code(error: &DraftError) -> &'static str {
- match error {
- DraftError::UnknownContract(_) => "unknown_contract",
- DraftError::ContractNotDraftAuthorable { .. } => "contract_not_draft_authorable",
- DraftError::ContractRegistryVersionMismatch { .. } => "contract_registry_version_mismatch",
- DraftError::DraftExpectedEventIdMismatch { .. } => "draft_expected_event_id_mismatch",
- DraftError::ContractKindMismatch { .. } => "contract_kind_mismatch",
- DraftError::ContractShape { error, .. } => error.code(),
- DraftError::SignedEventPubkeyMismatch { .. } => "signed_event_pubkey_mismatch",
- DraftError::SignedEventIdMismatch { .. } => "signed_event_id_mismatch",
- DraftError::SignedEventCreatedAtMismatch { .. } => "signed_event_created_at_mismatch",
- DraftError::SignedEventKindMismatch { .. } => "signed_event_kind_mismatch",
- DraftError::SignedEventTagsMismatch { .. } => "signed_event_tags_mismatch",
- DraftError::SignedEventContentMismatch { .. } => "signed_event_content_mismatch",
- DraftError::SignedEventComputedIdMismatch { .. } => "signed_event_computed_id_mismatch",
- DraftError::IdParse(_) => "id_parse",
- DraftError::CanonicalEventId(_) => "canonical_event_id",
- DraftError::Envelope(_) => "event_envelope",
- DraftError::SignedEvent(_) => "signed_event",
- }
-}
-
-fn builder_required<T>(
- value: Option<T>,
- field: &'static str,
-) -> Result<T, RadrootsKnowledgeBuilderError> {
- value.ok_or(RadrootsKnowledgeBuilderError::MissingField(field))
-}
-
-fn builder_required_string(
- value: Option<String>,
- field: &'static str,
-) -> Result<String, RadrootsKnowledgeBuilderError> {
- builder_non_empty_string(builder_required(value, field)?, field)
-}
-
-fn builder_non_empty_string(
- value: String,
- field: &'static str,
-) -> Result<String, RadrootsKnowledgeBuilderError> {
- if value.trim().is_empty() {
- Err(RadrootsKnowledgeBuilderError::MissingField(field))
- } else {
- Ok(value)
- }
-}
-
-fn builder_validation_error(error: KnowledgeValidationError) -> RadrootsKnowledgeBuilderError {
- match error {
- KnowledgeValidationError::EmptyField(field) => {
- RadrootsKnowledgeBuilderError::MissingField(field)
- }
- KnowledgeValidationError::InvalidField(field) => {
- RadrootsKnowledgeBuilderError::InvalidField(field)
- }
- }
-}
-
-fn builder_validated<T>(
- value: T,
- validate: fn(&T) -> Result<(), KnowledgeValidationError>,
-) -> Result<T, RadrootsKnowledgeBuilderError> {
- validate(&value).map_err(builder_validation_error)?;
- Ok(value)
-}
-
-pub mod prelude {
- pub use super::{
- AddressableRef, DraftError, EventDraft, EventEnvelope, EventRef, KIND_FILE_METADATA,
- KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION,
- KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, KIND_WIKI_MERGE_REQUEST,
- KIND_WIKI_REDIRECT, KNOWLEDGE_CLAIM_CONTRACT_ID, KNOWLEDGE_FIELD_REPORT_CONTRACT_ID,
- KNOWLEDGE_RELATION_CONTRACT_ID, KNOWLEDGE_REVIEW_CONTRACT_ID, KNOWLEDGE_SOURCE_CONTRACT_ID,
- KnowledgeChangeProposal, KnowledgeCitationSpan, KnowledgeClaim, KnowledgeCodec,
- KnowledgeDraftBuilder, KnowledgeEventBuilder, KnowledgeFieldContext, KnowledgeFieldReport,
- KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation,
- KnowledgeObservationValue, KnowledgeRelation, KnowledgeReview, KnowledgeReviewScope,
- KnowledgeReviewScore, KnowledgeReviewTarget, KnowledgeSource, KnowledgeValidationError,
- Nip01EventWireParts, RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA,
- RADROOTS_EVIDENCE_BOUNTY_SCHEMA, RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA,
- RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION,
- RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
- RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RADROOTS_WIKI_D_TAG_MAX_LEN,
- RadrootsContractValidatedEvent, RadrootsDecodeError, RadrootsDecodedEvent,
- RadrootsEncodeError, RadrootsIdVerifiedEvent, RadrootsKnowledgeBuilderError,
- RadrootsKnowledgeClaimBuilder, RadrootsKnowledgeContractManifest,
- RadrootsKnowledgeContractManifestEntry, RadrootsKnowledgeFieldReportBuilder,
- RadrootsKnowledgeManifestCodecSupport, RadrootsKnowledgeManifestDiscriminator,
- RadrootsKnowledgeManifestTagContract, RadrootsKnowledgeRelationBuilder,
- RadrootsKnowledgeReviewBuilder, RadrootsKnowledgeSourceBuilder,
- RadrootsNip01VerificationError, RadrootsSdkKnowledgeError, RadrootsSignatureVerifiedEvent,
- RadrootsWikiArticleBuilder, RadrootsWikiMergeRequestBuilder, RadrootsWikiRedirectBuilder,
- RightsAssertion, WIKI_ARTICLE_CONTRACT_ID, WIKI_MERGE_REQUEST_CONTRACT_ID,
- WIKI_REDIRECT_CONTRACT_ID, WikiArticle, WikiArticleVersionRef, WikiDTagError,
- WikiMergeRequest, WikiRedirect, build_knowledge_claim_event,
- build_knowledge_field_report_event, build_knowledge_relation_event,
- build_knowledge_review_event, build_knowledge_source_event, build_wiki_article_event,
- build_wiki_merge_request_event, build_wiki_redirect_event, contract_manifest,
- contract_manifest_json, contract_manifest_sha256, normalize_wiki_d_tag,
- prepare_knowledge_claim_draft, prepare_knowledge_field_report_draft,
- prepare_knowledge_relation_draft, prepare_knowledge_review_draft,
- prepare_knowledge_source_draft, prepare_wiki_article_draft,
- prepare_wiki_merge_request_draft, prepare_wiki_redirect_draft, validate_knowledge_claim,
- validate_wiki_article, validate_wiki_d_tag, verify_and_decode_radroots_event,
- };
-}
diff --git a/crates/sdk/src/privacy.rs b/crates/sdk/src/privacy.rs
@@ -1,181 +0,0 @@
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum ProductSensitivityField {
- ExactLocation,
- SensitiveFulfillmentDetails,
- PublicButSensitiveNotes,
- ProtocolMinimizedInventoryFields,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
-#[serde(rename_all = "snake_case")]
-#[non_exhaustive]
-pub enum PrivacyPreflightStatus {
- Ok,
- ExplicitConfirmationRequired,
- ForbiddenPublicFields,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct PrivacyPreflightReceipt {
- pub status: PrivacyPreflightStatus,
- pub fields: Vec<ProductSensitivityField>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
-pub struct PrivacyPreflightConfirmation {
- pub fields: Vec<ProductSensitivityField>,
-}
-
-#[cfg(feature = "runtime")]
-impl PrivacyPreflightConfirmation {
- pub fn new() -> Self {
- Self { fields: Vec::new() }
- }
-
- pub fn confirm(mut self, field: ProductSensitivityField) -> Self {
- self.fields.push(field);
- self.fields.sort();
- self.fields.dedup();
- self
- }
-
- pub fn confirms(&self, field: ProductSensitivityField) -> bool {
- self.fields.contains(&field)
- }
-}
-
-#[cfg(feature = "runtime")]
-impl PrivacyPreflightReceipt {
- pub fn evaluate<I>(fields: I) -> Self
- where
- I: IntoIterator<Item = ProductSensitivityField>,
- {
- let mut fields = fields.into_iter().collect::<Vec<_>>();
- fields.sort();
- fields.dedup();
- let status = if fields.iter().any(|field| {
- matches!(
- field,
- ProductSensitivityField::ExactLocation
- | ProductSensitivityField::SensitiveFulfillmentDetails
- )
- }) {
- PrivacyPreflightStatus::ForbiddenPublicFields
- } else if fields
- .iter()
- .any(|field| matches!(field, ProductSensitivityField::PublicButSensitiveNotes))
- {
- PrivacyPreflightStatus::ExplicitConfirmationRequired
- } else {
- PrivacyPreflightStatus::Ok
- };
- Self { status, fields }
- }
-
- pub fn require_public_publish_allowed(
- &self,
- operation: impl Into<String>,
- confirmation: &PrivacyPreflightConfirmation,
- ) -> Result<(), crate::error::RadrootsSdkError> {
- match self.status {
- PrivacyPreflightStatus::Ok => Ok(()),
- PrivacyPreflightStatus::ForbiddenPublicFields => {
- Err(crate::error::RadrootsSdkError::PrivacyPreflight {
- operation: operation.into(),
- status: self.status,
- fields: self.fields.clone(),
- })
- }
- PrivacyPreflightStatus::ExplicitConfirmationRequired => {
- let missing_fields = self
- .fields
- .iter()
- .copied()
- .filter(|field| privacy_field_requires_confirmation(*field))
- .filter(|field| !confirmation.confirms(*field))
- .collect::<Vec<_>>();
- if missing_fields.is_empty() {
- Ok(())
- } else {
- Err(crate::error::RadrootsSdkError::PrivacyPreflight {
- operation: operation.into(),
- status: self.status,
- fields: missing_fields,
- })
- }
- }
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn privacy_field_requires_confirmation(field: ProductSensitivityField) -> bool {
- matches!(field, ProductSensitivityField::PublicButSensitiveNotes)
-}
-
-#[cfg(test)]
-#[cfg(feature = "runtime")]
-mod tests {
- use super::{
- PrivacyPreflightConfirmation, PrivacyPreflightReceipt, PrivacyPreflightStatus,
- ProductSensitivityField,
- };
-
- #[test]
- fn privacy_preflight_classifies_public_sensitivity() {
- let ok = PrivacyPreflightReceipt::evaluate([
- ProductSensitivityField::ProtocolMinimizedInventoryFields,
- ]);
- assert_eq!(ok.status, PrivacyPreflightStatus::Ok);
-
- let confirm =
- PrivacyPreflightReceipt::evaluate([ProductSensitivityField::PublicButSensitiveNotes]);
- assert_eq!(
- confirm.status,
- PrivacyPreflightStatus::ExplicitConfirmationRequired
- );
-
- let forbidden = PrivacyPreflightReceipt::evaluate([
- ProductSensitivityField::ExactLocation,
- ProductSensitivityField::SensitiveFulfillmentDetails,
- ]);
- assert_eq!(
- forbidden.status,
- PrivacyPreflightStatus::ForbiddenPublicFields
- );
- }
-
- #[test]
- fn privacy_confirmation_allows_only_confirmable_public_fields() {
- let confirmation = PrivacyPreflightConfirmation::new()
- .confirm(ProductSensitivityField::PublicButSensitiveNotes);
- PrivacyPreflightReceipt::evaluate([ProductSensitivityField::PublicButSensitiveNotes])
- .require_public_publish_allowed("trade.test", &confirmation)
- .expect("confirmed public note");
- PrivacyPreflightReceipt::evaluate([
- ProductSensitivityField::PublicButSensitiveNotes,
- ProductSensitivityField::ProtocolMinimizedInventoryFields,
- ])
- .require_public_publish_allowed("trade.test", &confirmation)
- .expect("confirmed public note with protocol inventory");
-
- let missing =
- PrivacyPreflightReceipt::evaluate([ProductSensitivityField::PublicButSensitiveNotes])
- .require_public_publish_allowed("trade.test", &PrivacyPreflightConfirmation::new())
- .expect_err("missing confirmation");
- assert_eq!(missing.code(), "privacy_preflight");
-
- let forbidden = PrivacyPreflightReceipt::evaluate([
- ProductSensitivityField::SensitiveFulfillmentDetails,
- ])
- .require_public_publish_allowed("trade.test", &confirmation)
- .expect_err("forbidden cannot be confirmed");
- assert_eq!(forbidden.code(), "privacy_preflight");
- }
-}
diff --git a/crates/sdk/src/private_store.rs b/crates/sdk/src/private_store.rs
@@ -1,1065 +0,0 @@
-#![cfg(feature = "runtime")]
-
-// RCRV1-DEV-008: this module is the sole SDK quarantine for predecessor secret
-// storage until Step 179 transfers the private store into radroots_storage_sqlite.
-// New SDK secret integrations must use radroots_secrets.
-
-use crate::RadrootsSdkError;
-use radroots_event::envelope::kind::KIND_FARM;
-use radroots_event::id::{AddressableCoordinate, AddressableCoordinateParts};
-use radroots_event::trade::RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES;
-use radroots_protected_store::{RadrootsProtectedFileKeySource, RadrootsProtectedStoreEnvelope};
-use radroots_secret_vault::{RadrootsSecretKeyWrapping, RadrootsSecretVaultAccessError};
-use radroots_trade::evidence::RadrootsTradePrivateTermsEvidenceV1;
-use radroots_trade::model::RadrootsTradePrivateTermsStateV1;
-use serde::{Deserialize, Serialize};
-use sha2::{Digest, Sha256};
-use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
-use sqlx::{Row, SqlitePool};
-use std::path::Path;
-use std::str::FromStr;
-use std::sync::Arc;
-
-pub(crate) const SDK_PRIVATE_STORE_SCHEMA_VERSION: i64 = 1;
-
-const PRIVATE_STORE_KEY_VERSION: i64 = 1;
-const PRIVATE_STORE_FILE_CREDENTIAL_BACKEND: &str = "protected_file_wrapped_v1";
-const PRIVATE_STORE_MEMORY_CREDENTIAL_BACKEND: &str = "memory_test_wrapped_v1";
-
-const PRIVATE_STORE_MIGRATION_UP: &str = r#"
-CREATE TABLE IF NOT EXISTS private_metadata (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- schema_version INTEGER NOT NULL CHECK(schema_version = 1),
- profile_id BLOB NOT NULL CHECK(length(profile_id) = 16),
- runtime_contract_hash BLOB NOT NULL CHECK(length(runtime_contract_hash) = 32),
- key_version INTEGER NOT NULL CHECK(key_version > 0),
- sqlite_source_id TEXT NOT NULL,
- created_at_ms INTEGER NOT NULL,
- updated_at_ms INTEGER NOT NULL
-) STRICT;
-
-CREATE TABLE IF NOT EXISTS wrapped_profile_key (
- key_version INTEGER PRIMARY KEY CHECK(key_version > 0),
- credential_backend TEXT NOT NULL,
- wrapped_key BLOB NOT NULL,
- nonce BLOB NOT NULL CHECK(length(nonce) = 24),
- created_at_ms INTEGER NOT NULL,
- retired_at_ms INTEGER
-) STRICT;
-
-CREATE TABLE IF NOT EXISTS wrapped_signing_secret (
- account_id BLOB PRIMARY KEY CHECK(length(account_id) = 16),
- public_key BLOB NOT NULL UNIQUE CHECK(length(public_key) = 32),
- key_version INTEGER NOT NULL REFERENCES wrapped_profile_key(key_version),
- ciphertext BLOB NOT NULL,
- nonce BLOB NOT NULL CHECK(length(nonce) = 24),
- created_at_ms INTEGER NOT NULL,
- updated_at_ms INTEGER NOT NULL
-) STRICT;
-
-CREATE TABLE IF NOT EXISTS private_farm_location (
- farm_kind INTEGER NOT NULL CHECK(farm_kind = 30340),
- owner_pubkey BLOB NOT NULL CHECK(length(owner_pubkey) = 32),
- farm_d_tag TEXT NOT NULL,
- key_version INTEGER NOT NULL REFERENCES wrapped_profile_key(key_version),
- ciphertext BLOB NOT NULL,
- nonce BLOB NOT NULL CHECK(length(nonce) = 24),
- created_at_ms INTEGER NOT NULL,
- updated_at_ms INTEGER NOT NULL,
- PRIMARY KEY(farm_kind, owner_pubkey, farm_d_tag)
-) STRICT, WITHOUT ROWID;
-
-CREATE TABLE IF NOT EXISTS private_trade_artifacts (
- artifact_id TEXT PRIMARY KEY NOT NULL,
- trade_id TEXT NOT NULL CHECK(length(trade_id) = 32),
- candidate_id TEXT CHECK(candidate_id IS NULL OR length(candidate_id) = 64),
- artifact_kind TEXT NOT NULL CHECK(artifact_kind IN ('binding_terms','message','contact_bundle','delivery_instruction')),
- schema_id TEXT NOT NULL,
- ciphertext_commitment TEXT NOT NULL CHECK(length(ciphertext_commitment) = 64),
- key_version INTEGER NOT NULL REFERENCES wrapped_profile_key(key_version),
- ciphertext BLOB NOT NULL,
- encryption_metadata BLOB NOT NULL,
- retention_class TEXT NOT NULL,
- created_at_ms INTEGER NOT NULL,
- expires_at_ms INTEGER,
- deleted_at_ms INTEGER,
- UNIQUE(artifact_kind, ciphertext_commitment)
-) STRICT;
-
-CREATE INDEX IF NOT EXISTS private_trade_artifacts_trade_idx
- ON private_trade_artifacts(trade_id, candidate_id, artifact_kind, deleted_at_ms);
-
-CREATE INDEX IF NOT EXISTS private_trade_artifacts_expiry_idx
- ON private_trade_artifacts(expires_at_ms, artifact_id)
- WHERE expires_at_ms IS NOT NULL AND deleted_at_ms IS NULL;
-
-CREATE TABLE IF NOT EXISTS cursor_hmac_key (
- key_id BLOB PRIMARY KEY CHECK(length(key_id) = 16),
- key_version INTEGER NOT NULL REFERENCES wrapped_profile_key(key_version),
- ciphertext BLOB NOT NULL,
- nonce BLOB NOT NULL CHECK(length(nonce) = 24),
- created_at_ms INTEGER NOT NULL,
- retired_at_ms INTEGER
-) STRICT;
-
-CREATE TABLE IF NOT EXISTS nip46_session_private (
- session_id BLOB PRIMARY KEY CHECK(length(session_id) = 16),
- user_pubkey BLOB NOT NULL CHECK(length(user_pubkey) = 32),
- remote_signer_pubkey BLOB NOT NULL CHECK(length(remote_signer_pubkey) = 32),
- client_pubkey BLOB NOT NULL CHECK(length(client_pubkey) = 32),
- key_version INTEGER NOT NULL REFERENCES wrapped_profile_key(key_version),
- ciphertext BLOB NOT NULL,
- nonce BLOB NOT NULL CHECK(length(nonce) = 24),
- expires_at_ms INTEGER NOT NULL,
- status TEXT NOT NULL CHECK(status IN ('active','expired','revoked')),
- created_at_ms INTEGER NOT NULL,
- updated_at_ms INTEGER NOT NULL
-) STRICT;
-
-CREATE TABLE IF NOT EXISTS key_rotation_progress (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- from_key_version INTEGER NOT NULL,
- to_key_version INTEGER NOT NULL,
- table_name TEXT NOT NULL,
- last_primary_key BLOB,
- state TEXT NOT NULL CHECK(state IN ('running','verifying','complete','failed')),
- started_at_ms INTEGER NOT NULL,
- updated_at_ms INTEGER NOT NULL,
- error_code TEXT
-) STRICT;
-"#;
-
-#[derive(Clone)]
-pub(crate) struct SdkPrivateStore {
- pool: SqlitePool,
- key_source: SdkPrivateStoreKeySource,
- credential_backend: &'static str,
-}
-
-#[derive(Clone)]
-enum SdkPrivateStoreKeySource {
- Memory(Arc<SdkPrivateStoreMemoryKeySource>),
- File(RadrootsProtectedFileKeySource),
-}
-
-#[derive(Default)]
-struct SdkPrivateStoreMemoryKeySource;
-
-#[derive(Clone, Debug, PartialEq)]
-pub(crate) struct SdkPrivateFarmLocationRecord {
- pub farm_addr: AddressableCoordinate,
- pub farm_pubkey: String,
- pub farm_d_tag: String,
- pub label: Option<String>,
- pub latitude: f64,
- pub longitude: f64,
- pub locality_primary: String,
- pub locality_city: Option<String>,
- pub locality_region: Option<String>,
- pub locality_country: Option<String>,
- pub geohash5: String,
- pub geonames_feature_id: Option<i64>,
- pub geonames_country_id: Option<String>,
- pub updated_at_ms: i64,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct SdkPrivateStoreStatusSummary {
- pub farm_private_locations: i64,
- pub trade_private_artifacts: i64,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct SdkPrivateFarmLocationPayload {
- label: Option<String>,
- latitude: f64,
- longitude: f64,
- locality_primary: String,
- locality_city: Option<String>,
- locality_region: Option<String>,
- locality_country: Option<String>,
- geohash5: String,
- geonames_feature_id: Option<i64>,
- geonames_country_id: Option<String>,
- updated_at_ms: i64,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-pub(crate) enum SdkPrivateTradeArtifactKind {
- BindingTerms,
- Message,
- ContactBundle,
- DeliveryInstruction,
-}
-
-impl SdkPrivateTradeArtifactKind {
- pub(crate) fn as_str(self) -> &'static str {
- match self {
- Self::BindingTerms => "binding_terms",
- Self::Message => "message",
- Self::ContactBundle => "contact_bundle",
- Self::DeliveryInstruction => "delivery_instruction",
- }
- }
-
- fn from_str(value: &str) -> Result<Self, RadrootsSdkError> {
- match value {
- "binding_terms" => Ok(Self::BindingTerms),
- "message" => Ok(Self::Message),
- "contact_bundle" => Ok(Self::ContactBundle),
- "delivery_instruction" => Ok(Self::DeliveryInstruction),
- _ => Err(RadrootsSdkError::PrivateStore {
- message: format!("unknown private trade artifact kind `{value}`"),
- }),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct SdkPrivateTradeArtifactInput {
- pub artifact_id: String,
- pub trade_id: String,
- pub candidate_id: Option<String>,
- pub artifact_kind: SdkPrivateTradeArtifactKind,
- pub schema_id: String,
- pub plaintext: Vec<u8>,
- pub retention_class: String,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct SdkPrivateTradeArtifactRecord {
- pub artifact_id: String,
- pub trade_id: String,
- pub candidate_id: Option<String>,
- pub artifact_kind: SdkPrivateTradeArtifactKind,
- pub schema_id: String,
- pub ciphertext_commitment: String,
- pub plaintext: Vec<u8>,
- pub retention_class: String,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
- pub deleted_at_ms: Option<i64>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub(crate) struct SdkPrivateTradeArtifactMetadata {
- pub artifact_id: String,
- pub trade_id: String,
- pub candidate_id: Option<String>,
- pub artifact_kind: SdkPrivateTradeArtifactKind,
- pub schema_id: String,
- pub ciphertext_commitment: String,
- pub retention_class: String,
- pub created_at_ms: i64,
- pub expires_at_ms: Option<i64>,
- pub deleted_at_ms: Option<i64>,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct SdkPrivateTradeArtifactPayload {
- artifact_id: String,
- trade_id: String,
- artifact_kind: SdkPrivateTradeArtifactKind,
- schema_id: String,
- plaintext: Vec<u8>,
- retention_class: String,
- created_at_ms: i64,
- expires_at_ms: Option<i64>,
-}
-
-impl SdkPrivateStore {
- pub async fn open_memory() -> Result<Self, RadrootsSdkError> {
- let options =
- SqliteConnectOptions::from_str("sqlite::memory:").map_err(private_store_error)?;
- let pool = SqlitePoolOptions::new()
- .max_connections(1)
- .connect_with(options)
- .await
- .map_err(private_store_error)?;
- let store = Self {
- pool,
- key_source: SdkPrivateStoreKeySource::Memory(Arc::default()),
- credential_backend: PRIVATE_STORE_MEMORY_CREDENTIAL_BACKEND,
- };
- store.configure_connection(false).await?;
- store.apply_up().await?;
- Ok(store)
- }
-
- pub async fn open_file(path: impl AsRef<Path>) -> Result<Self, RadrootsSdkError> {
- let path = path.as_ref();
- let options = SqliteConnectOptions::new()
- .filename(path)
- .create_if_missing(true);
- let pool = SqlitePoolOptions::new()
- .max_connections(1)
- .connect_with(options)
- .await
- .map_err(private_store_error)?;
- let store = Self {
- pool,
- key_source: SdkPrivateStoreKeySource::File(
- RadrootsProtectedFileKeySource::from_sidecar_suffix(path, ".vault.key"),
- ),
- credential_backend: PRIVATE_STORE_FILE_CREDENTIAL_BACKEND,
- };
- store.configure_connection(true).await?;
- store.reject_pre_v1_private_store().await?;
- store.apply_up().await?;
- Ok(store)
- }
-
- pub fn pool(&self) -> &SqlitePool {
- &self.pool
- }
-
- pub async fn pragma_foreign_keys(&self) -> Result<i64, RadrootsSdkError> {
- query_i64(&self.pool, "PRAGMA foreign_keys").await
- }
-
- pub async fn pragma_busy_timeout(&self) -> Result<i64, RadrootsSdkError> {
- query_i64(&self.pool, "PRAGMA busy_timeout").await
- }
-
- pub async fn pragma_journal_mode(&self) -> Result<String, RadrootsSdkError> {
- query_string(&self.pool, "PRAGMA journal_mode").await
- }
-
- pub async fn status_summary(&self) -> Result<SdkPrivateStoreStatusSummary, RadrootsSdkError> {
- Ok(SdkPrivateStoreStatusSummary {
- farm_private_locations: query_i64(
- &self.pool,
- "SELECT COUNT(*) FROM private_farm_location",
- )
- .await?,
- trade_private_artifacts: query_i64(
- &self.pool,
- "SELECT COUNT(*) FROM private_trade_artifacts WHERE deleted_at_ms IS NULL",
- )
- .await?,
- })
- }
-
- pub async fn upsert_farm_location(
- &self,
- record: &SdkPrivateFarmLocationRecord,
- ) -> Result<(), RadrootsSdkError> {
- validate_location_record(record)?;
- let parts = farm_location_parts(&record.farm_addr)?;
- let owner_pubkey = parts.pubkey.as_bytes().to_vec();
- let envelope = self.seal_farm_location(record)?;
- let nonce = envelope.header.nonce.to_vec();
- let ciphertext = envelope.encode_json().map_err(private_store_error)?;
- sqlx::query(
- r#"
- INSERT INTO private_farm_location (
- farm_kind,
- owner_pubkey,
- farm_d_tag,
- key_version,
- ciphertext,
- nonce,
- created_at_ms,
- updated_at_ms
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
- ON CONFLICT(farm_kind, owner_pubkey, farm_d_tag) DO UPDATE SET
- key_version = excluded.key_version,
- ciphertext = excluded.ciphertext,
- nonce = excluded.nonce,
- updated_at_ms = excluded.updated_at_ms
- "#,
- )
- .bind(i64::from(KIND_FARM))
- .bind(owner_pubkey)
- .bind(parts.d_tag.as_str())
- .bind(PRIVATE_STORE_KEY_VERSION)
- .bind(ciphertext)
- .bind(nonce)
- .bind(record.updated_at_ms)
- .bind(record.updated_at_ms)
- .execute(&self.pool)
- .await
- .map(|_| ())
- .map_err(private_store_error)
- }
-
- pub async fn farm_location(
- &self,
- farm_addr: &AddressableCoordinate,
- ) -> Result<Option<SdkPrivateFarmLocationRecord>, RadrootsSdkError> {
- let parts = farm_location_parts(farm_addr)?;
- let owner_pubkey = parts.pubkey.as_bytes().to_vec();
- let row = sqlx::query(
- r#"
- SELECT ciphertext, nonce
- FROM private_farm_location
- WHERE farm_kind = ?1 AND owner_pubkey = ?2 AND farm_d_tag = ?3
- "#,
- )
- .bind(i64::from(KIND_FARM))
- .bind(owner_pubkey)
- .bind(parts.d_tag.as_str())
- .fetch_optional(&self.pool)
- .await
- .map_err(private_store_error)?;
- row.map(|row| self.private_farm_location_from_row(farm_addr.clone(), parts, row))
- .transpose()
- }
-
- pub async fn delete_farm_location(
- &self,
- farm_addr: &AddressableCoordinate,
- ) -> Result<bool, RadrootsSdkError> {
- let parts = farm_location_parts(farm_addr)?;
- let owner_pubkey = parts.pubkey.as_bytes().to_vec();
- sqlx::query(
- r#"
- DELETE FROM private_farm_location
- WHERE farm_kind = ?1 AND owner_pubkey = ?2 AND farm_d_tag = ?3
- "#,
- )
- .bind(i64::from(KIND_FARM))
- .bind(owner_pubkey)
- .bind(parts.d_tag.as_str())
- .execute(&self.pool)
- .await
- .map(|receipt| receipt.rows_affected() > 0)
- .map_err(private_store_error)
- }
-
- pub async fn upsert_trade_artifact(
- &self,
- input: &SdkPrivateTradeArtifactInput,
- ) -> Result<SdkPrivateTradeArtifactMetadata, RadrootsSdkError> {
- validate_trade_artifact_input(input)?;
- let envelope = self.seal_trade_artifact(input)?;
- let nonce = envelope.header.nonce.to_vec();
- let ciphertext = envelope.encode_json().map_err(private_store_error)?;
- let ciphertext_commitment = hex::encode(Sha256::digest(ciphertext.as_slice()));
- let encryption_metadata = serde_json::to_vec(&serde_json::json!({
- "key_version": PRIVATE_STORE_KEY_VERSION,
- "nonce": hex::encode(nonce.as_slice())
- }))
- .map_err(private_store_error)?;
- sqlx::query(
- r#"
- INSERT INTO private_trade_artifacts (
- artifact_id,
- trade_id,
- candidate_id,
- artifact_kind,
- schema_id,
- ciphertext_commitment,
- key_version,
- ciphertext,
- encryption_metadata,
- retention_class,
- created_at_ms,
- expires_at_ms,
- deleted_at_ms
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, NULL)
- ON CONFLICT(artifact_id) DO UPDATE SET
- trade_id = excluded.trade_id,
- candidate_id = excluded.candidate_id,
- artifact_kind = excluded.artifact_kind,
- schema_id = excluded.schema_id,
- ciphertext_commitment = excluded.ciphertext_commitment,
- key_version = excluded.key_version,
- ciphertext = excluded.ciphertext,
- encryption_metadata = excluded.encryption_metadata,
- retention_class = excluded.retention_class,
- created_at_ms = excluded.created_at_ms,
- expires_at_ms = excluded.expires_at_ms,
- deleted_at_ms = NULL
- "#,
- )
- .bind(input.artifact_id.as_str())
- .bind(input.trade_id.as_str())
- .bind(input.candidate_id.as_deref())
- .bind(input.artifact_kind.as_str())
- .bind(input.schema_id.as_str())
- .bind(ciphertext_commitment.as_str())
- .bind(PRIVATE_STORE_KEY_VERSION)
- .bind(ciphertext)
- .bind(encryption_metadata)
- .bind(input.retention_class.as_str())
- .bind(input.created_at_ms)
- .bind(input.expires_at_ms)
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- Ok(SdkPrivateTradeArtifactMetadata {
- artifact_id: input.artifact_id.clone(),
- trade_id: input.trade_id.clone(),
- candidate_id: input.candidate_id.clone(),
- artifact_kind: input.artifact_kind,
- schema_id: input.schema_id.clone(),
- ciphertext_commitment,
- retention_class: input.retention_class.clone(),
- created_at_ms: input.created_at_ms,
- expires_at_ms: input.expires_at_ms,
- deleted_at_ms: None,
- })
- }
-
- pub async fn trade_artifact(
- &self,
- artifact_id: &str,
- ) -> Result<Option<SdkPrivateTradeArtifactRecord>, RadrootsSdkError> {
- let row = sqlx::query(
- r#"
- SELECT artifact_id, trade_id, candidate_id, artifact_kind, schema_id,
- ciphertext_commitment, ciphertext, retention_class, created_at_ms,
- expires_at_ms, deleted_at_ms
- FROM private_trade_artifacts
- WHERE artifact_id = ?1
- "#,
- )
- .bind(artifact_id)
- .fetch_optional(&self.pool)
- .await
- .map_err(private_store_error)?;
- row.map(|row| self.trade_artifact_from_row(row)).transpose()
- }
-
- pub async fn trade_artifact_metadata_for_trade(
- &self,
- trade_id: &str,
- ) -> Result<Vec<SdkPrivateTradeArtifactMetadata>, RadrootsSdkError> {
- let rows = sqlx::query(
- r#"
- SELECT artifact_id, trade_id, candidate_id, artifact_kind, schema_id,
- ciphertext_commitment, retention_class, created_at_ms, expires_at_ms,
- deleted_at_ms
- FROM private_trade_artifacts
- WHERE trade_id = ?1
- ORDER BY created_at_ms, artifact_id
- "#,
- )
- .bind(trade_id)
- .fetch_all(&self.pool)
- .await
- .map_err(private_store_error)?;
- rows.into_iter()
- .map(trade_artifact_metadata_from_row)
- .collect()
- }
-
- pub async fn delete_trade_artifact(
- &self,
- artifact_id: &str,
- deleted_at_ms: i64,
- ) -> Result<bool, RadrootsSdkError> {
- sqlx::query(
- r#"
- UPDATE private_trade_artifacts
- SET deleted_at_ms = ?2
- WHERE artifact_id = ?1 AND deleted_at_ms IS NULL
- "#,
- )
- .bind(artifact_id)
- .bind(deleted_at_ms)
- .execute(&self.pool)
- .await
- .map(|receipt| receipt.rows_affected() > 0)
- .map_err(private_store_error)
- }
-
- pub async fn private_terms_evidence(
- &self,
- trade_id: &str,
- candidate_id: &str,
- artifact_id: &str,
- schema_id: &str,
- ciphertext_commitment: &str,
- ) -> Result<RadrootsTradePrivateTermsEvidenceV1, RadrootsSdkError> {
- let state = match self.trade_artifact(artifact_id).await? {
- None => RadrootsTradePrivateTermsStateV1::Missing,
- Some(record) => {
- if record.deleted_at_ms.is_some()
- || record.trade_id != trade_id
- || record.artifact_kind != SdkPrivateTradeArtifactKind::BindingTerms
- || record
- .candidate_id
- .as_deref()
- .is_some_and(|stored_candidate_id| stored_candidate_id != candidate_id)
- {
- RadrootsTradePrivateTermsStateV1::Missing
- } else if record.schema_id != schema_id
- || record.ciphertext_commitment != ciphertext_commitment
- {
- RadrootsTradePrivateTermsStateV1::CommitmentMismatch
- } else {
- RadrootsTradePrivateTermsStateV1::AvailableVerified
- }
- }
- };
- Ok(RadrootsTradePrivateTermsEvidenceV1::new(
- candidate_id
- .parse()
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("private terms candidate id is invalid: {error}"),
- })?,
- state,
- ))
- }
-
- async fn configure_connection(&self, file_backed: bool) -> Result<(), RadrootsSdkError> {
- sqlx::query("PRAGMA foreign_keys = ON")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query("PRAGMA busy_timeout = 5000")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query("PRAGMA trusted_schema = OFF")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query("PRAGMA temp_store = MEMORY")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query("PRAGMA secure_delete = FAST")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- if file_backed {
- sqlx::query("PRAGMA journal_mode = WAL")
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- }
- Ok(())
- }
-
- async fn apply_up(&self) -> Result<(), RadrootsSdkError> {
- sqlx::raw_sql(PRIVATE_STORE_MIGRATION_UP)
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query(
- r#"
- INSERT OR IGNORE INTO private_metadata (
- singleton,
- schema_version,
- profile_id,
- runtime_contract_hash,
- key_version,
- sqlite_source_id,
- created_at_ms,
- updated_at_ms
- ) VALUES (
- 1,
- 1,
- randomblob(16),
- zeroblob(32),
- 1,
- sqlite_source_id(),
- CAST(strftime('%s','now') AS INTEGER) * 1000,
- CAST(strftime('%s','now') AS INTEGER) * 1000
- )
- "#,
- )
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query(
- r#"
- INSERT OR IGNORE INTO wrapped_profile_key (
- key_version,
- credential_backend,
- wrapped_key,
- nonce,
- created_at_ms,
- retired_at_ms
- ) VALUES (
- 1,
- ?1,
- zeroblob(1),
- randomblob(24),
- CAST(strftime('%s','now') AS INTEGER) * 1000,
- NULL
- )
- "#,
- )
- .bind(self.credential_backend)
- .execute(&self.pool)
- .await
- .map_err(private_store_error)?;
- sqlx::query("PRAGMA user_version = 1")
- .execute(&self.pool)
- .await
- .map(|_| ())
- .map_err(private_store_error)
- }
-
- async fn reject_pre_v1_private_store(&self) -> Result<(), RadrootsSdkError> {
- let exists = query_i64(
- &self.pool,
- "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'sdk_private_farm_location'",
- )
- .await?;
- if exists != 0 {
- return Err(RadrootsSdkError::UnsupportedProfileSchema {
- path: Path::new("private.sqlite").to_path_buf(),
- message: "pre-V1 SDK private store is unsupported by release-product v1".to_owned(),
- });
- }
- Ok(())
- }
-
- fn seal_farm_location(
- &self,
- record: &SdkPrivateFarmLocationRecord,
- ) -> Result<RadrootsProtectedStoreEnvelope, RadrootsSdkError> {
- let payload = SdkPrivateFarmLocationPayload {
- label: record.label.clone(),
- latitude: record.latitude,
- longitude: record.longitude,
- locality_primary: record.locality_primary.clone(),
- locality_city: record.locality_city.clone(),
- locality_region: record.locality_region.clone(),
- locality_country: record.locality_country.clone(),
- geohash5: record.geohash5.clone(),
- geonames_feature_id: record.geonames_feature_id,
- geonames_country_id: record.geonames_country_id.clone(),
- updated_at_ms: record.updated_at_ms,
- };
- let plaintext = serde_json::to_vec(&payload).map_err(private_store_error)?;
- RadrootsProtectedStoreEnvelope::seal_with_wrapped_key(
- &self.key_source,
- farm_location_key_slot(record.farm_addr.as_str()).as_str(),
- plaintext.as_slice(),
- )
- .map_err(private_store_error)
- }
-
- fn seal_trade_artifact(
- &self,
- input: &SdkPrivateTradeArtifactInput,
- ) -> Result<RadrootsProtectedStoreEnvelope, RadrootsSdkError> {
- let payload = SdkPrivateTradeArtifactPayload {
- artifact_id: input.artifact_id.clone(),
- trade_id: input.trade_id.clone(),
- artifact_kind: input.artifact_kind,
- schema_id: input.schema_id.clone(),
- plaintext: input.plaintext.clone(),
- retention_class: input.retention_class.clone(),
- created_at_ms: input.created_at_ms,
- expires_at_ms: input.expires_at_ms,
- };
- let plaintext = serde_json::to_vec(&payload).map_err(private_store_error)?;
- RadrootsProtectedStoreEnvelope::seal_with_wrapped_key(
- &self.key_source,
- trade_artifact_key_slot(input.artifact_id.as_str()).as_str(),
- plaintext.as_slice(),
- )
- .map_err(private_store_error)
- }
-
- fn private_farm_location_from_row(
- &self,
- farm_addr: AddressableCoordinate,
- parts: AddressableCoordinateParts,
- row: sqlx::sqlite::SqliteRow,
- ) -> Result<SdkPrivateFarmLocationRecord, RadrootsSdkError> {
- let ciphertext: Vec<u8> = row.try_get("ciphertext").map_err(private_store_error)?;
- let nonce: Vec<u8> = row.try_get("nonce").map_err(private_store_error)?;
- let envelope = RadrootsProtectedStoreEnvelope::decode_json(ciphertext.as_slice())
- .map_err(private_store_error)?;
- if envelope.header.nonce.as_slice() != nonce.as_slice() {
- return Err(RadrootsSdkError::PrivateStore {
- message: "private farm location envelope nonce does not match row nonce".to_owned(),
- });
- }
- let plaintext = envelope
- .open_with_wrapped_key(&self.key_source)
- .map_err(private_store_error)?;
- let payload: SdkPrivateFarmLocationPayload =
- serde_json::from_slice(plaintext.as_slice()).map_err(private_store_error)?;
- Ok(SdkPrivateFarmLocationRecord {
- farm_addr,
- farm_pubkey: parts.pubkey.to_hex(),
- farm_d_tag: parts.d_tag.as_str().to_owned(),
- label: payload.label,
- latitude: payload.latitude,
- longitude: payload.longitude,
- locality_primary: payload.locality_primary,
- locality_city: payload.locality_city,
- locality_region: payload.locality_region,
- locality_country: payload.locality_country,
- geohash5: payload.geohash5,
- geonames_feature_id: payload.geonames_feature_id,
- geonames_country_id: payload.geonames_country_id,
- updated_at_ms: payload.updated_at_ms,
- })
- }
-
- fn trade_artifact_from_row(
- &self,
- row: sqlx::sqlite::SqliteRow,
- ) -> Result<SdkPrivateTradeArtifactRecord, RadrootsSdkError> {
- let artifact_id: String = row.try_get("artifact_id").map_err(private_store_error)?;
- let trade_id: String = row.try_get("trade_id").map_err(private_store_error)?;
- let candidate_id: Option<String> =
- row.try_get("candidate_id").map_err(private_store_error)?;
- let artifact_kind = SdkPrivateTradeArtifactKind::from_str(
- row.try_get::<String, _>("artifact_kind")
- .map_err(private_store_error)?
- .as_str(),
- )?;
- let schema_id: String = row.try_get("schema_id").map_err(private_store_error)?;
- let ciphertext_commitment: String = row
- .try_get("ciphertext_commitment")
- .map_err(private_store_error)?;
- let ciphertext: Vec<u8> = row.try_get("ciphertext").map_err(private_store_error)?;
- if hex::encode(Sha256::digest(ciphertext.as_slice())) != ciphertext_commitment {
- return Err(RadrootsSdkError::PrivateStore {
- message:
- "private trade artifact ciphertext commitment does not match row commitment"
- .to_owned(),
- });
- }
- let envelope = RadrootsProtectedStoreEnvelope::decode_json(ciphertext.as_slice())
- .map_err(private_store_error)?;
- let plaintext = envelope
- .open_with_wrapped_key(&self.key_source)
- .map_err(private_store_error)?;
- let payload: SdkPrivateTradeArtifactPayload =
- serde_json::from_slice(plaintext.as_slice()).map_err(private_store_error)?;
- if payload.artifact_id != artifact_id
- || payload.trade_id != trade_id
- || payload.artifact_kind != artifact_kind
- || payload.schema_id != schema_id
- {
- return Err(RadrootsSdkError::PrivateStore {
- message: "private trade artifact envelope metadata does not match row metadata"
- .to_owned(),
- });
- }
- Ok(SdkPrivateTradeArtifactRecord {
- artifact_id,
- trade_id,
- candidate_id,
- artifact_kind,
- schema_id,
- ciphertext_commitment,
- plaintext: payload.plaintext,
- retention_class: row
- .try_get("retention_class")
- .map_err(private_store_error)?,
- created_at_ms: row.try_get("created_at_ms").map_err(private_store_error)?,
- expires_at_ms: row.try_get("expires_at_ms").map_err(private_store_error)?,
- deleted_at_ms: row.try_get("deleted_at_ms").map_err(private_store_error)?,
- })
- }
-}
-
-impl RadrootsSecretKeyWrapping for SdkPrivateStoreKeySource {
- type Error = RadrootsSecretVaultAccessError;
-
- fn wrap_data_key(&self, key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> {
- match self {
- Self::Memory(source) => source.wrap_data_key(key_slot, plaintext_key),
- Self::File(source) => source.wrap_data_key(key_slot, plaintext_key),
- }
- }
-
- fn unwrap_data_key(&self, key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> {
- match self {
- Self::Memory(source) => source.unwrap_data_key(key_slot, wrapped_key),
- Self::File(source) => source.unwrap_data_key(key_slot, wrapped_key),
- }
- }
-}
-
-impl RadrootsSecretKeyWrapping for SdkPrivateStoreMemoryKeySource {
- type Error = RadrootsSecretVaultAccessError;
-
- fn wrap_data_key(&self, _key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> {
- Ok(plaintext_key.to_vec())
- }
-
- fn unwrap_data_key(&self, _key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> {
- Ok(wrapped_key.to_vec())
- }
-}
-
-async fn query_i64(pool: &SqlitePool, sql: &'static str) -> Result<i64, RadrootsSdkError> {
- let row = sqlx::query(sql)
- .fetch_one(pool)
- .await
- .map_err(private_store_error)?;
- row.try_get(0).map_err(private_store_error)
-}
-
-async fn query_string(pool: &SqlitePool, sql: &'static str) -> Result<String, RadrootsSdkError> {
- let row = sqlx::query(sql)
- .fetch_one(pool)
- .await
- .map_err(private_store_error)?;
- row.try_get(0).map_err(private_store_error)
-}
-
-fn farm_location_parts(
- farm_addr: &AddressableCoordinate,
-) -> Result<AddressableCoordinateParts, RadrootsSdkError> {
- let parts = AddressableCoordinateParts::parse(farm_addr.as_str()).map_err(|error| {
- RadrootsSdkError::InvalidRequest {
- message: format!("farm address is invalid: {error}"),
- }
- })?;
- if parts.kind != KIND_FARM {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "private farm location address kind must be {KIND_FARM}, got {}",
- parts.kind
- ),
- });
- }
- Ok(parts)
-}
-
-fn farm_location_key_slot(farm_addr: &str) -> String {
- format!("private_farm_location:{farm_addr}")
-}
-
-fn trade_artifact_key_slot(artifact_id: &str) -> String {
- format!("private_trade_artifact:{artifact_id}")
-}
-
-fn validate_location_record(record: &SdkPrivateFarmLocationRecord) -> Result<(), RadrootsSdkError> {
- if !record.latitude.is_finite()
- || !record.longitude.is_finite()
- || record.latitude < -90.0
- || record.latitude > 90.0
- || record.longitude < -180.0
- || record.longitude > 180.0
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm exact location coordinates are outside valid latitude/longitude bounds"
- .to_owned(),
- });
- }
- if record.locality_primary.trim().is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm public locality primary name must not be empty".to_owned(),
- });
- }
- if record.geohash5.len() != 5 {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm public locality geohash must be precision 5".to_owned(),
- });
- }
- if record
- .label
- .as_deref()
- .is_some_and(|label| label.trim().is_empty())
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm private location label must not be empty".to_owned(),
- });
- }
- let parts = farm_location_parts(&record.farm_addr)?;
- if parts.pubkey.to_hex() != record.farm_pubkey {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm private location address pubkey does not match record pubkey".to_owned(),
- });
- }
- if parts.d_tag.as_str() != record.farm_d_tag {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm private location address d tag does not match record d tag".to_owned(),
- });
- }
- Ok(())
-}
-
-fn validate_trade_artifact_input(
- input: &SdkPrivateTradeArtifactInput,
-) -> Result<(), RadrootsSdkError> {
- for (field, value) in [
- ("artifact_id", input.artifact_id.as_str()),
- ("trade_id", input.trade_id.as_str()),
- ("schema_id", input.schema_id.as_str()),
- ("retention_class", input.retention_class.as_str()),
- ] {
- if value.trim().is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!("private trade artifact {field} must not be empty"),
- });
- }
- }
- if input.plaintext.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "private trade artifact plaintext must not be empty".to_owned(),
- });
- }
- if input.plaintext.len() > RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "private trade artifact plaintext exceeds {RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES} bytes"
- ),
- });
- }
- if input
- .expires_at_ms
- .is_some_and(|expires| expires <= input.created_at_ms)
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "private trade artifact expiration must be after creation time".to_owned(),
- });
- }
- if input.trade_id.len() != 32 || !input.trade_id.chars().all(|c| c.is_ascii_hexdigit()) {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "private trade artifact trade_id must be 32 hex characters".to_owned(),
- });
- }
- if input.candidate_id.as_deref().is_some_and(|candidate_id| {
- candidate_id.len() != 64 || !candidate_id.chars().all(|c| c.is_ascii_hexdigit())
- }) {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "private trade artifact candidate_id must be 64 hex characters".to_owned(),
- });
- }
- Ok(())
-}
-
-fn trade_artifact_metadata_from_row(
- row: sqlx::sqlite::SqliteRow,
-) -> Result<SdkPrivateTradeArtifactMetadata, RadrootsSdkError> {
- Ok(SdkPrivateTradeArtifactMetadata {
- artifact_id: row.try_get("artifact_id").map_err(private_store_error)?,
- trade_id: row.try_get("trade_id").map_err(private_store_error)?,
- candidate_id: row.try_get("candidate_id").map_err(private_store_error)?,
- artifact_kind: SdkPrivateTradeArtifactKind::from_str(
- row.try_get::<String, _>("artifact_kind")
- .map_err(private_store_error)?
- .as_str(),
- )?,
- schema_id: row.try_get("schema_id").map_err(private_store_error)?,
- ciphertext_commitment: row
- .try_get("ciphertext_commitment")
- .map_err(private_store_error)?,
- retention_class: row
- .try_get("retention_class")
- .map_err(private_store_error)?,
- created_at_ms: row.try_get("created_at_ms").map_err(private_store_error)?,
- expires_at_ms: row.try_get("expires_at_ms").map_err(private_store_error)?,
- deleted_at_ms: row.try_get("deleted_at_ms").map_err(private_store_error)?,
- })
-}
-
-fn private_store_error(error: impl ToString) -> RadrootsSdkError {
- RadrootsSdkError::PrivateStore {
- message: error.to_string(),
- }
-}
-
-#[cfg(test)]
-#[path = "../tests/unit/private_store_tests.rs"]
-mod tests;
diff --git a/crates/sdk/src/product_clients.rs b/crates/sdk/src/product_clients.rs
@@ -1,80 +0,0 @@
-#[cfg(feature = "runtime")]
-use crate::RadrootsClient;
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct FarmsClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> FarmsClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct ListingsClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> ListingsClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct MarketClient<'client> {
- pub(crate) _sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> MarketClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { _sdk: sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct GeoNamesClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> GeoNamesClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct TradesClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> TradesClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy)]
-pub struct SyncClient<'client> {
- pub(crate) sdk: &'client RadrootsClient,
-}
-
-#[cfg(feature = "runtime")]
-impl<'client> SyncClient<'client> {
- pub(crate) fn new(sdk: &'client RadrootsClient) -> Self {
- Self { sdk }
- }
-}
diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs
@@ -1,1097 +0,0 @@
-#[cfg(feature = "signer-adapters")]
-use crate::RadrootsSdkSignRequest;
-use crate::{
- RadrootsClient, RadrootsSdkError, ReticulumBehavior, SatisfactionPolicy, SdkIdempotencyKey,
- TargetPolicy, TargetSet, TransportProfile,
- runtime::{RuntimeRecoveryReceiptWrite, record_runtime_recovery_receipt, sdk_now_ms},
-};
-use radroots_event::{
- draft::{EventDraft, SignedEvent},
- envelope::{EventKind, EventKindClass},
- id::EventId,
-};
-use radroots_event_store::{
- RadrootsEventIngest, RadrootsEventPersistence, RadrootsEventStoreError,
- RadrootsTransportObservation, RadrootsTransportObservationType,
-};
-use radroots_outbox::{
- RadrootsOutboxDeliveryPlanInput, RadrootsOutboxEnqueueStatus, RadrootsOutboxReticulumBehavior,
- RadrootsOutboxSignedOperationInput, RadrootsOutboxSignedTradeMutationInput,
-};
-use radroots_protocol::runtime::v1::OperationId;
-use radroots_signing::{
- Actor, SignRequest, Signer,
- request::{CancellationPolicy, SignPolicy},
-};
-use radroots_transport::{Target, TransportId};
-use radroots_transport_reticulum::RADROOTS_RETICULUM_ENDPOINT_URI;
-use sha2::{Digest, Sha256};
-use sqlx::Row;
-
-const SDK_LOCAL_EVENT_ENDPOINT_URI: &str = "local:sdk";
-const SDK_RUNTIME_CONTRACT_VERSION: &str = "1";
-
-pub(crate) struct SdkWorkflowEnqueueRequest<'a> {
- pub(crate) operation_kind: &'static str,
- pub(crate) actor: &'a Actor,
- pub(crate) frozen_draft: &'a EventDraft,
- pub(crate) target_policy: TargetPolicy,
- pub(crate) satisfaction_policy: SatisfactionPolicy,
- pub(crate) idempotency_key: Option<SdkIdempotencyKey>,
-}
-
-#[derive(Debug)]
-pub(crate) struct SdkWorkflowEnqueueReceipt {
- pub(crate) signed_event_id: EventId,
- pub(crate) local_event_seq: i64,
- pub(crate) outbox_operation_id: i64,
- pub(crate) outbox_event_id: i64,
- pub(crate) state: RadrootsOutboxEnqueueStatus,
- pub(crate) idempotency_digest_prefix: String,
-}
-
-pub(crate) async fn enqueue_signed_workflow(
- sdk: &RadrootsClient,
- request: SdkWorkflowEnqueueRequest<'_>,
- signer: &dyn Signer,
-) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- ensure_durable_workflow_kind(&request)?;
- let delivery_plan =
- resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
- let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
- if let Some(receipt) = prepared.committed_receipt {
- return Ok(receipt);
- }
- mark_runtime_operation_state(
- sdk,
- &request,
- &prepared.idempotency_key,
- SdkRuntimeOperationState::SignaturePending,
- None,
- )
- .await?;
- let sign_request = signing_request(&request)?;
- let signed_event = match signer.sign(sign_request).await {
- Ok(receipt) => receipt.signed_event().clone(),
- Err(error) => {
- let sdk_error: RadrootsSdkError = error.into();
- record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &sdk_error)
- .await?;
- return Err(sdk_error);
- }
- };
- match enqueue_signed_workflow_event(sdk, &request, signed_event, delivery_plan).await {
- Ok(receipt) => Ok(receipt),
- Err(error) => {
- record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
- .await?;
- Err(error)
- }
- }
-}
-
-fn signing_request(
- request: &SdkWorkflowEnqueueRequest<'_>,
-) -> Result<SignRequest, RadrootsSdkError> {
- let operation_id = signing_operation_id(request.operation_kind).ok_or_else(|| {
- RadrootsSdkError::InvalidRequest {
- message: format!("unknown signing operation `{}`", request.operation_kind),
- }
- })?;
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .map_err(|_| RadrootsSdkError::ClockBeforeUnixEpoch)?
- .as_secs();
- let deadline_unix = now
- .checked_add(30)
- .ok_or(RadrootsSdkError::TimestampOutOfRange { value: now })?;
- let policy = SignPolicy::new(deadline_unix, CancellationPolicy::PreservePublishedRequest)?;
- SignRequest::new(
- operation_id,
- request.actor.clone(),
- request.frozen_draft.clone(),
- policy,
- )
- .map_err(Into::into)
-}
-
-pub(crate) fn signing_operation_id(operation: &str) -> Option<OperationId> {
- match operation {
- "farm.publish" | "farm.publish.v1" => Some(OperationId::FarmPublish),
- "listing.publish" | "listing.publish.v1" => Some(OperationId::ListingPublish),
- "trade.proposal.submit" | "trade.submit_proposal.v1" => {
- Some(OperationId::TradeProposalSubmit)
- }
- "trade.revision.propose" | "trade.propose_revision.v1" => {
- Some(OperationId::TradeRevisionPropose)
- }
- "trade.candidate.decide" | "trade.decide_candidate.v1" => {
- Some(OperationId::TradeCandidateDecide)
- }
- "trade.cancellation.submit" | "trade.cancel.v1" => {
- Some(OperationId::TradeCancellationSubmit)
- }
- "trade.operation.resume" | "trade.resume_operation.v1" => {
- Some(OperationId::TradeOperationResume)
- }
- "sync.push" | "sync.push.v1" => Some(OperationId::SyncPush),
- _ => OperationId::parse(operation.strip_suffix(".v1").unwrap_or(operation)).ok(),
- }
-}
-
-#[cfg(feature = "signer-adapters")]
-pub(crate) async fn enqueue_configured_signed_workflow(
- sdk: &RadrootsClient,
- request: SdkWorkflowEnqueueRequest<'_>,
-) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- ensure_durable_workflow_kind(&request)?;
- let delivery_plan =
- resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
- let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
- if let Some(receipt) = prepared.committed_receipt {
- return Ok(receipt);
- }
- mark_runtime_operation_state(
- sdk,
- &request,
- &prepared.idempotency_key,
- SdkRuntimeOperationState::SignaturePending,
- None,
- )
- .await?;
- let signed_event = match sdk
- .sign_with_configured_signer(RadrootsSdkSignRequest::new(
- request.operation_kind,
- request.actor,
- request.frozen_draft,
- ))
- .await
- {
- Ok(receipt) => receipt.signed_event,
- Err(error) => {
- record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
- .await?;
- return Err(error);
- }
- };
- match enqueue_signed_workflow_event(sdk, &request, signed_event, delivery_plan).await {
- Ok(receipt) => Ok(receipt),
- Err(error) => {
- record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &error)
- .await?;
- Err(error)
- }
- }
-}
-
-async fn enqueue_signed_workflow_event(
- sdk: &RadrootsClient,
- request: &SdkWorkflowEnqueueRequest<'_>,
- signed_event: SignedEvent,
- delivery_plan: SdkResolvedDeliveryPlan,
-) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- if radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS
- .contains(&request.frozen_draft.kind_u32())
- {
- return enqueue_signed_trade_workflow_event(sdk, request, signed_event, delivery_plan)
- .await;
- }
- let idempotency_key =
- request
- .idempotency_key
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!(
- "{} requires an explicit UUIDv7 idempotency key",
- request.operation_kind
- ),
- })?;
- let observed_at_ms = sdk_now_ms(sdk)?;
- let signed_event_id = EventId::parse(request.frozen_draft.expected_event_id_hex())
- .expect("frozen workflow draft has a valid expected event id");
- let delivery_plan_value = delivery_plan.delivery_plan;
- let mut tx =
- sdk._event_store
- .pool()
- .begin()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- ensure_runtime_operation_can_commit(&mut tx, request, &idempotency_key).await?;
- let local_import_observation = RadrootsTransportObservation::new(
- TransportId::LOCAL,
- SDK_LOCAL_EVENT_ENDPOINT_URI,
- RadrootsTransportObservationType::LocalImport,
- observed_at_ms,
- )?;
- let ingest =
- workflow_event_ingest(request.operation_kind, signed_event.clone(), observed_at_ms)?
- .with_observation(local_import_observation);
- let ingest_receipt = sdk
- ._event_store
- .ingest_event_in_transaction(&mut tx, ingest)
- .await?;
- let (event_store_inserted, local_event_seq) = durable_event_persistence(
- ingest_receipt.event_id.as_str(),
- &ingest_receipt.persistence,
- )?;
- let outbox_input = signed_outbox_input(
- request.operation_kind,
- request.frozen_draft,
- signed_event,
- delivery_plan_value,
- idempotency_key.clone(),
- event_store_inserted,
- observed_at_ms,
- );
- let outbox_receipt = sdk
- ._outbox
- .enqueue_signed_operation_in_transaction(&mut tx, outbox_input)
- .await?;
- let idempotency_digest_prefix =
- digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
- let receipt = SdkWorkflowEnqueueReceipt {
- signed_event_id,
- local_event_seq,
- outbox_operation_id: outbox_receipt.operation_id,
- outbox_event_id: outbox_receipt.outbox_event_id,
- state: outbox_receipt.status,
- idempotency_digest_prefix,
- };
- commit_runtime_operation_journal(&mut tx, request, &idempotency_key, &receipt, observed_at_ms)
- .await?;
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(receipt)
-}
-
-async fn enqueue_signed_trade_workflow_event(
- sdk: &RadrootsClient,
- request: &SdkWorkflowEnqueueRequest<'_>,
- signed_event: SignedEvent,
- delivery_plan: SdkResolvedDeliveryPlan,
-) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- let idempotency_key =
- request
- .idempotency_key
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!(
- "{} requires an explicit UUIDv7 idempotency key",
- request.operation_kind
- ),
- })?;
- let observed_at_ms = sdk_now_ms(sdk)?;
- let signed_event_id = EventId::parse(request.frozen_draft.expected_event_id_hex())
- .expect("frozen workflow draft has a valid expected event id");
- let delivery_plan_value = delivery_plan.delivery_plan;
- let mut tx =
- sdk._event_store
- .pool()
- .begin()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- ensure_runtime_operation_can_commit(&mut tx, request, &idempotency_key).await?;
- let local_import_observation = RadrootsTransportObservation::new(
- TransportId::LOCAL,
- SDK_LOCAL_EVENT_ENDPOINT_URI,
- RadrootsTransportObservationType::LocalImport,
- observed_at_ms,
- )?;
- let ingest =
- workflow_event_ingest(request.operation_kind, signed_event.clone(), observed_at_ms)?
- .with_observation(local_import_observation);
- let ingest_receipt = sdk
- ._event_store
- .ingest_event_in_transaction(&mut tx, ingest)
- .await?;
- let (event_store_inserted, local_event_seq) = durable_event_persistence(
- ingest_receipt.event_id.as_str(),
- &ingest_receipt.persistence,
- )?;
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- let outbox_input = signed_trade_outbox_input(
- request.operation_kind,
- request.frozen_draft,
- signed_event,
- delivery_plan_value,
- idempotency_key.clone(),
- event_store_inserted,
- observed_at_ms,
- )?;
- let outbox_receipt = sdk
- ._outbox
- .enqueue_signed_trade_mutation_operation(outbox_input)
- .await?;
- let idempotency_digest_prefix =
- digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
- let receipt = SdkWorkflowEnqueueReceipt {
- signed_event_id,
- local_event_seq,
- outbox_operation_id: outbox_receipt.operation_id,
- outbox_event_id: outbox_receipt.outbox_event_id,
- state: outbox_receipt.status,
- idempotency_digest_prefix,
- };
- let mut tx =
- sdk._event_store
- .pool()
- .begin()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- commit_runtime_operation_journal(&mut tx, request, &idempotency_key, &receipt, observed_at_ms)
- .await?;
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(receipt)
-}
-
-struct SdkResolvedDeliveryPlan {
- delivery_plan: RadrootsOutboxDeliveryPlanInput,
-}
-
-fn resolved_delivery_plan(
- sdk: &RadrootsClient,
- target_policy: &TargetPolicy,
- satisfaction_policy: &SatisfactionPolicy,
-) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> {
- match target_policy {
- TargetPolicy::Explicit(target_policy) => {
- let targets = target_policy.clone().into_targets();
- let reticulum_behavior =
- reticulum_behavior_for_targets(sdk.transport_profile(), &targets);
- delivery_plan_from_targets("explicit", targets, satisfaction_policy, reticulum_behavior)
- }
- TargetPolicy::DefaultProfile => {
- let transport_profile = sdk.transport_profile();
- let targets = transport_profile
- .target_set()?
- .map(TargetSet::into_targets)
- .unwrap_or_default();
- if targets.is_empty() && !satisfaction_policy.is_no_wait() {
- return Err(RadrootsSdkError::empty_transport_targets(
- "publish transport profile",
- ));
- }
- delivery_plan_from_targets(
- transport_profile.transport_profile_id(),
- targets,
- satisfaction_policy,
- outbox_reticulum_behavior(transport_profile),
- )
- }
- TargetPolicy::LocalOnly => {
- if !satisfaction_policy.is_no_wait() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "local-only target policy requires no_wait satisfaction policy"
- .to_owned(),
- });
- }
- delivery_plan_from_targets(
- "local_only",
- Vec::new(),
- satisfaction_policy,
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts,
- )
- }
- TargetPolicy::MeshScope(scope) => {
- let target_set = TargetSet::transport_targets(vec![Target::new_with_metadata(
- TransportId::RETICULUM,
- RADROOTS_RETICULUM_ENDPOINT_URI,
- Some(scope.transport_scope()),
- None,
- )?])?;
- delivery_plan_from_targets(
- "mesh_scope",
- target_set.into_targets(),
- satisfaction_policy,
- outbox_reticulum_behavior(sdk.transport_profile()),
- )
- }
- }
-}
-
-fn delivery_plan_from_targets(
- transport_profile_id: impl Into<String>,
- targets: Vec<Target>,
- satisfaction_policy: &SatisfactionPolicy,
- reticulum_behavior: RadrootsOutboxReticulumBehavior,
-) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> {
- let delivery_plan = RadrootsOutboxDeliveryPlanInput::new(
- transport_profile_id,
- 1,
- satisfaction_policy.transport_satisfaction_policy()?,
- targets,
- )
- .with_reticulum_behavior(reticulum_behavior);
- Ok(SdkResolvedDeliveryPlan { delivery_plan })
-}
-
-fn reticulum_behavior_for_targets(
- transport_profile: &TransportProfile,
- targets: &[Target],
-) -> RadrootsOutboxReticulumBehavior {
- if targets
- .iter()
- .any(|target| target.kind() == &TransportId::RETICULUM)
- {
- outbox_reticulum_behavior(transport_profile)
- } else {
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts
- }
-}
-
-fn outbox_reticulum_behavior(
- transport_profile: &TransportProfile,
-) -> RadrootsOutboxReticulumBehavior {
- match transport_profile {
- TransportProfile::Reticulum { profile } => reticulum_behavior(profile.behavior()),
- TransportProfile::MultiTarget { profile } => {
- reticulum_behavior(profile.reticulum().behavior())
- }
- TransportProfile::LocalOnly | TransportProfile::Nostr { .. } => {
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts
- }
- }
-}
-
-fn reticulum_behavior(behavior: ReticulumBehavior) -> RadrootsOutboxReticulumBehavior {
- match behavior {
- ReticulumBehavior::RejectDeliveryAttempts => {
- RadrootsOutboxReticulumBehavior::RejectDeliveryAttempts
- }
- ReticulumBehavior::DeferDeliveryPlans => {
- RadrootsOutboxReticulumBehavior::DeferDeliveryPlans
- }
- }
-}
-
-fn digest_prefix(digest: &str) -> String {
- digest.chars().take(12).collect()
-}
-
-fn ensure_durable_workflow_kind(
- request: &SdkWorkflowEnqueueRequest<'_>,
-) -> Result<(), RadrootsSdkError> {
- if EventKind::new(request.frozen_draft.kind_u32()).class() == EventKindClass::Ephemeral {
- return Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "{} cannot enqueue ephemeral event kind {} into a durable workflow",
- request.operation_kind,
- request.frozen_draft.kind_u32()
- ),
- });
- }
- Ok(())
-}
-
-fn workflow_event_ingest(
- operation_kind: &str,
- signed_event: SignedEvent,
- observed_at_ms: i64,
-) -> Result<RadrootsEventIngest, RadrootsSdkError> {
- RadrootsEventIngest::from_signed_event(signed_event, observed_at_ms).map_err(
- |error| match error {
- RadrootsEventStoreError::Nip01Verification(error) => {
- RadrootsSdkError::SignerReturnedEventDrift {
- operation: operation_kind.to_owned(),
- reason: format!(
- "signer returned an event that failed NIP-01 verification: {error}"
- ),
- }
- }
- error => error.into(),
- },
- )
-}
-
-fn durable_event_persistence(
- event_id: &str,
- persistence: &RadrootsEventPersistence,
-) -> Result<(bool, i64), RadrootsSdkError> {
- match persistence {
- RadrootsEventPersistence::Inserted { seq } => Ok((true, *seq)),
- RadrootsEventPersistence::Duplicate { seq } => Ok((false, *seq)),
- RadrootsEventPersistence::NotPersisted => Err(RadrootsSdkError::InvalidRequest {
- message: format!(
- "workflow event `{event_id}` requires durable local event-store persistence"
- ),
- }),
- }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-enum SdkRuntimeOperationState {
- Prepared,
- SignaturePending,
- Committed,
- Rejected,
- FailedRecoverable,
-}
-
-impl SdkRuntimeOperationState {
- fn as_str(self) -> &'static str {
- match self {
- Self::Prepared => "prepared",
- Self::SignaturePending => "signature_pending",
- Self::Committed => "committed",
- Self::Rejected => "rejected",
- Self::FailedRecoverable => "failed_recoverable",
- }
- }
-
- fn from_str(value: &str) -> Result<Self, RadrootsSdkError> {
- match value {
- "prepared" => Ok(Self::Prepared),
- "signature_pending" => Ok(Self::SignaturePending),
- "committed" => Ok(Self::Committed),
- "rejected" => Ok(Self::Rejected),
- "failed_recoverable" => Ok(Self::FailedRecoverable),
- _ => Err(RadrootsSdkError::EventStore {
- message: format!("unknown SDK runtime operation state `{value}`"),
- }),
- }
- }
-}
-
-struct PreparedRuntimeOperation {
- idempotency_key: SdkIdempotencyKey,
- committed_receipt: Option<SdkWorkflowEnqueueReceipt>,
-}
-
-#[cfg(test)]
-fn parse_event_id(value: &str, field: &str) -> Result<EventId, RadrootsSdkError> {
- EventId::parse(value).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("{field} is invalid: {error}"),
- })
-}
-
-fn signed_outbox_input(
- operation_kind: &'static str,
- frozen_draft: &EventDraft,
- signed_event: SignedEvent,
- delivery_plan: RadrootsOutboxDeliveryPlanInput,
- idempotency_key: SdkIdempotencyKey,
- event_store_inserted: bool,
- observed_at_ms: i64,
-) -> RadrootsOutboxSignedOperationInput {
- RadrootsOutboxSignedOperationInput::new(
- operation_kind,
- frozen_draft.clone(),
- signed_event,
- delivery_plan,
- event_store_inserted,
- observed_at_ms,
- observed_at_ms,
- )
- .with_idempotency_key(idempotency_key.into_string())
-}
-
-fn signed_trade_outbox_input(
- operation_kind: &'static str,
- frozen_draft: &EventDraft,
- signed_event: SignedEvent,
- delivery_plan: RadrootsOutboxDeliveryPlanInput,
- idempotency_key: SdkIdempotencyKey,
- event_store_inserted: bool,
- observed_at_ms: i64,
-) -> Result<RadrootsOutboxSignedTradeMutationInput, RadrootsSdkError> {
- let envelope =
- radroots_event::trade::trade_mutation_from_canonical_content(frozen_draft.content())
- .map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("trade mutation draft content is invalid: {error}"),
- })?;
- let mutation_id = envelope
- .mutation_id
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: "trade mutation draft content is missing mutation id".to_owned(),
- })?;
- Ok(RadrootsOutboxSignedTradeMutationInput::new(
- operation_kind,
- envelope.trade_id,
- mutation_id,
- hex::encode(Sha256::digest(frozen_draft.content().as_bytes())),
- frozen_draft.clone(),
- signed_event,
- delivery_plan,
- event_store_inserted,
- observed_at_ms,
- observed_at_ms,
- )
- .with_idempotency_key(idempotency_key.into_string()))
-}
-
-async fn prepare_runtime_operation_journal(
- sdk: &RadrootsClient,
- request: &SdkWorkflowEnqueueRequest<'_>,
- delivery_plan: &SdkResolvedDeliveryPlan,
-) -> Result<PreparedRuntimeOperation, RadrootsSdkError> {
- let idempotency_key =
- request
- .idempotency_key
- .clone()
- .ok_or_else(|| RadrootsSdkError::InvalidRequest {
- message: format!(
- "{} requires an explicit UUIDv7 idempotency key",
- request.operation_kind
- ),
- })?;
- let observed_at_ms = sdk_now_ms(sdk)?;
- let command_hash = runtime_request_digest(request, &delivery_plan.delivery_plan);
- let frozen_draft_json = frozen_draft_json(request.frozen_draft)?;
- let expected_transport_id = request.frozen_draft.expected_event_id_hex();
- let mut tx =
- sdk._event_store
- .pool()
- .begin()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- let committed_receipt = if let Some(row) = sqlx::query(
- "SELECT command_payload_hash, state, result_json FROM sdk_runtime_operation_journal WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
- )
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .fetch_optional(&mut *tx)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })? {
- let existing_digest: String = row
- .try_get("command_payload_hash")
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- if existing_digest != command_hash {
- let existing_digest_prefix = digest_prefix(existing_digest.as_str());
- let new_digest_prefix = digest_prefix(command_hash.as_str());
- let error = RadrootsSdkError::IdempotencyConflict {
- operation_kind: request.operation_kind.to_owned(),
- expected_pubkey_prefix: request.actor.public_key().to_hex().chars().take(12).collect(),
- existing_digest_prefix: existing_digest_prefix.clone(),
- new_digest_prefix: new_digest_prefix.clone(),
- };
- sqlx::query(
- "INSERT INTO sdk_runtime_recovery_receipt(recovery_code, operation_kind, actor_pubkey, idempotency_key, recovery_action, detail_json, created_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
- )
- .bind("idempotency_conflict")
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .bind("retry_operation_with_same_idempotency_key")
- .bind(
- serde_json::json!({
- "existing_digest_prefix": existing_digest_prefix,
- "new_digest_prefix": new_digest_prefix
- })
- .to_string(),
- )
- .bind(observed_at_ms)
- .execute(&mut *tx)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- return Err(error);
- }
- let state: String = row.try_get("state").map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- if SdkRuntimeOperationState::from_str(state.as_str())?
- == SdkRuntimeOperationState::Committed
- {
- let result_json: String =
- row.try_get("result_json")
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Some(workflow_receipt_from_result_json(result_json.as_str())?)
- } else {
- sqlx::query(
- "UPDATE sdk_runtime_operation_journal SET frozen_draft_json = ?, expected_transport_id = ?, state = ?, last_error_code = NULL, last_error_detail = NULL, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
- )
- .bind(frozen_draft_json.as_str())
- .bind(expected_transport_id)
- .bind(SdkRuntimeOperationState::Prepared.as_str())
- .bind(observed_at_ms)
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .execute(&mut *tx)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- None
- }
- } else {
- sqlx::query(
- "INSERT INTO sdk_runtime_operation_journal(contract_version, operation_kind, actor_pubkey, idempotency_key, command_payload_hash, frozen_draft_json, expected_transport_id, mutation_id, state, result_json, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
- )
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .bind(command_hash.as_str())
- .bind(frozen_draft_json.as_str())
- .bind(expected_transport_id)
- .bind(mutation_id_from_draft(request.frozen_draft))
- .bind(SdkRuntimeOperationState::Prepared.as_str())
- .bind(Option::<String>::None)
- .bind(observed_at_ms)
- .bind(observed_at_ms)
- .execute(&mut *tx)
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- None
- };
- tx.commit()
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- Ok(PreparedRuntimeOperation {
- idempotency_key,
- committed_receipt,
- })
-}
-
-async fn mark_runtime_operation_state(
- sdk: &RadrootsClient,
- request: &SdkWorkflowEnqueueRequest<'_>,
- idempotency_key: &SdkIdempotencyKey,
- state: SdkRuntimeOperationState,
- error: Option<&RadrootsSdkError>,
-) -> Result<(), RadrootsSdkError> {
- let observed_at_ms = sdk_now_ms(sdk)?;
- let (last_error_code, last_error_detail) = match error {
- Some(error) => (
- Some(error.code().to_owned()),
- Some(error.detail_json().to_string()),
- ),
- None => (None, None),
- };
- sqlx::query(
- "UPDATE sdk_runtime_operation_journal SET state = ?, last_error_code = ?, last_error_detail = ?, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
- )
- .bind(state.as_str())
- .bind(last_error_code)
- .bind(last_error_detail)
- .bind(observed_at_ms)
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .execute(sdk._event_store.pool())
- .await
- .map(|_| ())
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })
-}
-
-async fn record_runtime_operation_failure(
- sdk: &RadrootsClient,
- request: &SdkWorkflowEnqueueRequest<'_>,
- idempotency_key: &SdkIdempotencyKey,
- error: &RadrootsSdkError,
-) -> Result<(), RadrootsSdkError> {
- let state = match error {
- RadrootsSdkError::SignerRequestRejected { .. }
- | RadrootsSdkError::SignerReturnedEventDrift { .. }
- | RadrootsSdkError::SignerPubkeyMismatch { .. }
- | RadrootsSdkError::UnauthorizedActor { .. } => SdkRuntimeOperationState::Rejected,
- _ => SdkRuntimeOperationState::FailedRecoverable,
- };
- mark_runtime_operation_state(sdk, request, idempotency_key, state, Some(error)).await?;
- let recovery = match error {
- RadrootsSdkError::SignerRequestTimedOut { .. } => Some((
- "signer_timeout",
- "retry_operation_with_same_idempotency_key",
- )),
- RadrootsSdkError::IdempotencyConflict { .. } => Some((
- "idempotency_conflict",
- "retry_operation_with_same_idempotency_key",
- )),
- _ => None,
- };
- if let Some((recovery_code, recovery_action)) = recovery {
- let actor_pubkey = request.actor.public_key().to_hex();
- record_runtime_recovery_receipt(
- sdk._event_store.pool(),
- RuntimeRecoveryReceiptWrite {
- recovery_code,
- operation_kind: Some(request.operation_kind),
- actor_pubkey: Some(actor_pubkey.as_str()),
- idempotency_key: Some(idempotency_key.as_str()),
- recovery_action,
- detail_json: error.detail_json(),
- created_at_ms: sdk_now_ms(sdk)?,
- },
- )
- .await?;
- }
- Ok(())
-}
-
-async fn ensure_runtime_operation_can_commit(
- tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
- request: &SdkWorkflowEnqueueRequest<'_>,
- idempotency_key: &SdkIdempotencyKey,
-) -> Result<(), RadrootsSdkError> {
- let row = sqlx::query(
- "SELECT state FROM sdk_runtime_operation_journal WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
- )
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .fetch_one(tx.as_mut())
- .await
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- let state: String = row
- .try_get("state")
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- match SdkRuntimeOperationState::from_str(state.as_str())? {
- SdkRuntimeOperationState::Prepared
- | SdkRuntimeOperationState::SignaturePending
- | SdkRuntimeOperationState::FailedRecoverable => Ok(()),
- SdkRuntimeOperationState::Committed => Err(RadrootsSdkError::InvalidRequest {
- message: "SDK runtime operation is already committed".to_owned(),
- }),
- SdkRuntimeOperationState::Rejected => Err(RadrootsSdkError::InvalidRequest {
- message: "SDK runtime operation is rejected".to_owned(),
- }),
- }
-}
-
-async fn commit_runtime_operation_journal(
- tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
- request: &SdkWorkflowEnqueueRequest<'_>,
- idempotency_key: &SdkIdempotencyKey,
- receipt: &SdkWorkflowEnqueueReceipt,
- observed_at_ms: i64,
-) -> Result<(), RadrootsSdkError> {
- let result_json = workflow_receipt_result_json(receipt);
- sqlx::query(
- "UPDATE sdk_runtime_operation_journal SET state = ?, result_json = ?, last_error_code = NULL, last_error_detail = NULL, updated_at_ms = ? WHERE contract_version = ? AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
- )
- .bind(SdkRuntimeOperationState::Committed.as_str())
- .bind(result_json.to_string())
- .bind(observed_at_ms)
- .bind(SDK_RUNTIME_CONTRACT_VERSION)
- .bind(request.operation_kind)
- .bind(request.actor.public_key().to_hex())
- .bind(idempotency_key.as_str())
- .execute(tx.as_mut())
- .await
- .map(|_| ())
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })
-}
-
-fn workflow_receipt_result_json(receipt: &SdkWorkflowEnqueueReceipt) -> serde_json::Value {
- serde_json::json!({
- "api_version": 1,
- "state": "committed",
- "signed_event_id": receipt.signed_event_id.to_hex(),
- "local_event_seq": receipt.local_event_seq,
- "outbox_operation_id": receipt.outbox_operation_id,
- "outbox_event_id": receipt.outbox_event_id,
- "outbox_state": outbox_enqueue_status_str(receipt.state),
- "idempotency_digest_prefix": receipt.idempotency_digest_prefix
- })
-}
-
-fn workflow_receipt_from_result_json(
- result_json: &str,
-) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
- let value: serde_json::Value =
- serde_json::from_str(result_json).map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })?;
- let signed_event_id = value
- .get("signed_event_id")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing signed_event_id".to_owned(),
- })?;
- let local_event_seq = value
- .get("local_event_seq")
- .and_then(serde_json::Value::as_i64)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing local_event_seq".to_owned(),
- })?;
- let outbox_operation_id = value
- .get("outbox_operation_id")
- .and_then(serde_json::Value::as_i64)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing outbox_operation_id".to_owned(),
- })?;
- let outbox_event_id = value
- .get("outbox_event_id")
- .and_then(serde_json::Value::as_i64)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing outbox_event_id".to_owned(),
- })?;
- let outbox_state = value
- .get("outbox_state")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing outbox_state".to_owned(),
- })?;
- let idempotency_digest_prefix = value
- .get("idempotency_digest_prefix")
- .and_then(serde_json::Value::as_str)
- .ok_or_else(|| RadrootsSdkError::EventStore {
- message: "committed SDK operation receipt is missing idempotency_digest_prefix"
- .to_owned(),
- })?
- .to_owned();
- Ok(SdkWorkflowEnqueueReceipt {
- signed_event_id: EventId::parse(signed_event_id).map_err(|error| {
- RadrootsSdkError::EventStore {
- message: error.to_string(),
- }
- })?,
- local_event_seq,
- outbox_operation_id,
- outbox_event_id,
- state: outbox_enqueue_status_from_str(outbox_state)?,
- idempotency_digest_prefix,
- })
-}
-
-fn outbox_enqueue_status_str(status: RadrootsOutboxEnqueueStatus) -> &'static str {
- match status {
- RadrootsOutboxEnqueueStatus::Inserted => "inserted",
- RadrootsOutboxEnqueueStatus::Existing => "existing",
- }
-}
-
-fn outbox_enqueue_status_from_str(
- status: &str,
-) -> Result<RadrootsOutboxEnqueueStatus, RadrootsSdkError> {
- match status {
- "inserted" => Ok(RadrootsOutboxEnqueueStatus::Inserted),
- "existing" => Ok(RadrootsOutboxEnqueueStatus::Existing),
- _ => Err(RadrootsSdkError::EventStore {
- message: format!("unknown outbox enqueue status `{status}`"),
- }),
- }
-}
-
-fn frozen_draft_json(frozen_draft: &EventDraft) -> Result<String, RadrootsSdkError> {
- serde_json::to_string(&serde_json::json!({
- "contract_id": frozen_draft.contract_id(),
- "contract_registry_version": frozen_draft.contract_registry_version(),
- "kind": frozen_draft.kind_u32(),
- "created_at": frozen_draft.created_at_u64(),
- "tags": frozen_draft.tags_as_vec(),
- "content": frozen_draft.content(),
- "expected_pubkey": frozen_draft.expected_pubkey().to_hex(),
- "expected_event_id": frozen_draft.expected_event_id_hex()
- }))
- .map_err(|error| RadrootsSdkError::EventStore {
- message: error.to_string(),
- })
-}
-
-fn mutation_id_from_draft(frozen_draft: &EventDraft) -> Option<String> {
- if !radroots_event::envelope::kind::TRADE_MUTATION_EVENT_KINDS
- .contains(&frozen_draft.kind_u32())
- {
- return None;
- }
- radroots_event::trade::trade_mutation_from_canonical_content(frozen_draft.content())
- .ok()
- .and_then(|envelope| {
- envelope
- .mutation_id
- .map(|mutation_id| mutation_id.to_string())
- })
-}
-
-fn runtime_request_digest(
- request: &SdkWorkflowEnqueueRequest<'_>,
- delivery_plan: &RadrootsOutboxDeliveryPlanInput,
-) -> String {
- let mut targets = delivery_plan
- .targets
- .iter()
- .map(|target| {
- serde_json::json!({
- "kind": target.kind().canonical_label(),
- "uri": target.uri().as_str(),
- "scope": target.scope().map(|scope| scope.as_str()),
- "label": target.label().map(|label| label.as_str()),
- "fingerprint": target.fingerprint().as_str()
- })
- })
- .collect::<Vec<_>>();
- targets.sort_by(|left, right| {
- left.get("fingerprint")
- .and_then(serde_json::Value::as_str)
- .cmp(&right.get("fingerprint").and_then(serde_json::Value::as_str))
- });
- let digest_document = serde_json::json!({
- "contract_version": SDK_RUNTIME_CONTRACT_VERSION,
- "operation_kind": request.operation_kind,
- "actor_pubkey": request.actor.public_key().to_hex(),
- "draft": {
- "contract_id": request.frozen_draft.contract_id(),
- "contract_registry_version": request.frozen_draft.contract_registry_version(),
- "kind": request.frozen_draft.kind_u32(),
- "created_at": request.frozen_draft.created_at_u64(),
- "tags": request.frozen_draft.tags_as_vec(),
- "content_sha256": hex::encode(Sha256::digest(request.frozen_draft.content().as_bytes())),
- "expected_pubkey": request.frozen_draft.expected_pubkey().to_hex(),
- "expected_event_id": request.frozen_draft.expected_event_id_hex()
- },
- "delivery_plan": {
- "transport_profile_id": delivery_plan.transport_profile_id.as_str(),
- "target_policy_version": delivery_plan.target_policy_version,
- "satisfaction_policy": &delivery_plan.satisfaction_policy,
- "reticulum_behavior": delivery_plan.reticulum_behavior.as_str(),
- "targets": targets
- }
- });
- let bytes =
- serde_json::to_vec(&digest_document).expect("runtime journal digest document serializes");
- hex::encode(Sha256::digest(bytes))
-}
-
-#[cfg(test)]
-#[path = "../tests/unit/workflow_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/tests/geonames.rs b/crates/sdk/tests/geonames.rs
@@ -1,229 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use radroots_sdk::{
- GEONAMES_ASSET_HOST, GEONAMES_ASSET_VERSION, GeoNamesAssetFetcher, GeoNamesAssetSpec,
- GeoNamesAssetState, GeocoderError, RadrootsClient, RadrootsGeoNamesConfig, RadrootsSdkError,
- RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, RadrootsSdkRecoveryAction,
-};
-
-const TEST_SPEC: GeoNamesAssetSpec = GeoNamesAssetSpec {
- version: "test",
- file_name: "geonames-test.db",
- url: "https://assets.radroots.io/data/geonames/geonames-test.db",
- allowed_host: "assets.radroots.io",
- byte_size: 4,
- sha256: "53bc5cce8c5764019bb4ce6e597ec3885b71608668c9b6ef4940d364d7a914fa",
-};
-
-const BAD_HOST_SPEC: GeoNamesAssetSpec = GeoNamesAssetSpec {
- version: "bad-host",
- file_name: "geonames-bad-host.db",
- url: "https://example.com/data/geonames/geonames-bad-host.db",
- allowed_host: "assets.radroots.io",
- byte_size: 4,
- sha256: "53bc5cce8c5764019bb4ce6e597ec3885b71608668c9b6ef4940d364d7a914fa",
-};
-
-struct BytesFetcher(Vec<u8>);
-
-impl GeoNamesAssetFetcher for BytesFetcher {
- fn fetch(&self, _url: &str) -> Result<Vec<u8>, GeocoderError> {
- Ok(self.0.clone())
- }
-}
-
-fn geonames_error<T>(result: Result<T, RadrootsSdkError>) -> RadrootsSdkError {
- match result {
- Ok(_) => panic!("expected GeoNames error"),
- Err(error) => error,
- }
-}
-
-#[tokio::test]
-async fn sdk_geonames_client_resolves_shared_cache_paths_and_reports_missing_state() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let cache_root = tempdir.path().join("cache");
- let sdk = RadrootsClient::builder()
- .geonames_cache_root(cache_root.clone())
- .build()
- .await
- .expect("sdk");
- let geonames = sdk.geonames();
-
- assert_eq!(GEONAMES_ASSET_HOST, "assets.radroots.io");
- assert_eq!(GEONAMES_ASSET_VERSION, "1.0");
- assert_eq!(
- sdk.geonames_config(),
- Some(&RadrootsGeoNamesConfig::new(cache_root.clone()))
- );
- assert_eq!(
- geonames.root_path().expect("geonames root"),
- cache_root.join("shared").join("geonames")
- );
- assert_eq!(
- geonames.database_path().expect("geonames database path"),
- cache_root
- .join("shared")
- .join("geonames")
- .join("geonames-1.0.db")
- );
- assert_eq!(
- geonames
- .database_path_for_version("1.1")
- .expect("geonames version path"),
- cache_root
- .join("shared")
- .join("geonames")
- .join("geonames-1.1.db")
- );
-
- let status = geonames.inspect().expect("inspection");
- assert_eq!(status.state, GeoNamesAssetState::Missing);
- assert_eq!(status.version, "1.0");
- assert_eq!(
- status.path,
- cache_root
- .join("shared")
- .join("geonames")
- .join("geonames-1.0.db")
- );
-}
-
-#[tokio::test]
-async fn sdk_geonames_client_reports_missing_config_as_structured_error() {
- let sdk = RadrootsClient::builder().build().await.expect("sdk");
- let geonames = sdk.geonames();
- assert!(geonames.config().is_none());
- for error in [
- geonames.root_path().expect_err("missing root path config"),
- geonames
- .database_path()
- .expect_err("missing geonames config"),
- geonames
- .database_path_for_version("1.1")
- .expect_err("missing version path config"),
- geonames.inspect().expect_err("missing inspect config"),
- geonames.ensure().expect_err("missing ensure config"),
- geonames
- .ensure_with_fetcher(&BytesFetcher(b"bad!".to_vec()))
- .expect_err("missing ensure fetcher config"),
- geonames
- .ensure_with_spec_and_fetcher(&TEST_SPEC, &BytesFetcher(b"bad!".to_vec()))
- .expect_err("missing ensure spec config"),
- ] {
- assert_missing_config_error(error);
- }
-
- let error = geonames
- .database_path()
- .expect_err("missing geonames config");
-
- assert_missing_config_error(error);
-}
-
-fn assert_missing_config_error(error: RadrootsSdkError) {
- match &error {
- RadrootsSdkError::GeoNames { kind, .. } => {
- assert_eq!(*kind, RadrootsSdkGeoNamesErrorKind::Configuration);
- }
- other => panic!("expected geonames config error, got {other}"),
- }
- assert_eq!(error.code(), "geonames_configuration");
- assert_eq!(error.class(), RadrootsSdkErrorClass::Configuration);
- assert!(!error.retryable());
- assert_eq!(
- error.recovery_actions(),
- vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache]
- );
- assert_eq!(error.detail_json()["detail"]["kind"], "configuration");
-}
-
-#[tokio::test]
-async fn sdk_geonames_client_inspects_and_ensures_versioned_assets_without_network() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let cache_root = tempdir.path().join("cache");
- let sdk = RadrootsClient::builder()
- .geonames_config(RadrootsGeoNamesConfig::new(cache_root.clone()))
- .build()
- .await
- .expect("sdk");
- let geonames = sdk.geonames();
- let custom_path = cache_root.join("custom").join(TEST_SPEC.file_name);
-
- let missing = geonames
- .inspect_path_with_spec(&custom_path, &TEST_SPEC)
- .expect("missing inspect");
- assert_eq!(missing.state, GeoNamesAssetState::Missing);
- assert_eq!(missing.version, "test");
- assert_eq!(missing.path, custom_path);
-
- let default_ensure = geonames
- .ensure_with_fetcher(&BytesFetcher(b"bad!".to_vec()))
- .expect_err("default ensure invalid sqlite");
- assert!(matches!(
- default_ensure,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Integrity,
- ..
- }
- ));
-
- let ensure_error = geonames
- .ensure_with_spec_and_fetcher(&TEST_SPEC, &BytesFetcher(b"bad!".to_vec()))
- .expect_err("ensure invalid sqlite");
- assert!(matches!(
- ensure_error,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Integrity,
- ..
- }
- ));
-
- std::fs::create_dir_all(custom_path.parent().expect("custom parent")).expect("custom parent");
- std::fs::write(&custom_path, b"bad!").expect("invalid sqlite");
- let invalid = geonames
- .inspect_path_with_spec(&custom_path, &TEST_SPEC)
- .expect("invalid inspect");
- assert_eq!(invalid.state, GeoNamesAssetState::Invalid);
- assert_eq!(invalid.byte_size, Some(4));
- assert_eq!(
- invalid.sha256.as_deref(),
- Some("53bc5cce8c5764019bb4ce6e597ec3885b71608668c9b6ef4940d364d7a914fa")
- );
- assert!(
- invalid
- .validation_error
- .expect("validation error")
- .contains("SQLite")
- );
-
- let open_error =
- geonames_error(geonames.open_verified_path_with_spec(&custom_path, &TEST_SPEC));
- assert!(matches!(
- open_error,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Integrity,
- ..
- }
- ));
-
- let default_open_error = geonames_error(geonames.open_verified());
- assert!(matches!(
- default_open_error,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Cache,
- ..
- }
- ));
-
- let config_error = geonames
- .ensure_with_spec_and_fetcher(&BAD_HOST_SPEC, &BytesFetcher(b"bad!".to_vec()))
- .expect_err("bad host");
- assert!(matches!(
- config_error,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Configuration,
- ..
- }
- ));
-}
diff --git a/crates/sdk/tests/identity_public_api.rs b/crates/sdk/tests/identity_public_api.rs
@@ -1,33 +0,0 @@
-#![cfg(feature = "identity-models")]
-
-#[test]
-fn identity_models_are_public_through_identity_module() {
- use radroots_sdk::identity::{
- AccountId, Error, IdentityId, Profile, PublicIdentity, PublicKey, Username,
- username::{MAX_LENGTH, MIN_LENGTH},
- };
-
- const { assert!(MIN_LENGTH <= MAX_LENGTH) };
-
- let username = Username::parse(" Field_User ").expect("normalized username");
- assert_eq!(username.as_str(), "field_user");
-
- let public_key =
- PublicKey::from_hex("585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df")
- .expect("valid public key");
- let identity_id = IdentityId::from(public_key);
- let public_identity = PublicIdentity::new(public_key)
- .with_profile(Profile::new().with_username(username.clone()));
- let account_id = AccountId::from(&public_identity);
-
- assert_eq!(identity_id, public_identity.id());
- assert_eq!(account_id.to_hex(), public_key.to_hex());
- assert_eq!(
- public_identity.profile().and_then(Profile::username),
- Some(&username)
- );
- assert!(matches!(
- PublicKey::from_hex("not-a-public-key"),
- Err(Error::InvalidHexLength { .. })
- ));
-}
diff --git a/crates/sdk/tests/identity_retired_surface.rs b/crates/sdk/tests/identity_retired_surface.rs
@@ -1,118 +0,0 @@
-#![cfg(feature = "identity-models")]
-
-use std::{
- fs,
- path::{Path, PathBuf},
-};
-
-const RETIRED_IDENTIFIERS: &[&str] = &[
- "IdentityError",
- "RadrootsEncryptedIdentityFile",
- "RadrootsIdentity",
- "RadrootsIdentityEncryptedSecretKeyOptions",
- "RadrootsIdentityEncryptedSecretKeySecurity",
- "RadrootsIdentityFile",
- "RadrootsIdentityId",
- "RadrootsIdentityProfile",
- "RadrootsIdentityPublic",
- "RadrootsIdentitySecretKeyFormat",
- "RadrootsPublicKey",
-];
-
-#[test]
-fn sdk_production_surfaces_do_not_restore_retired_identity_apis() {
- let workspace_root = workspace_root();
- let mut sources = Vec::new();
- for root in ["crates", "packages"] {
- collect_production_sources(&workspace_root.join(root), &mut sources);
- }
- assert!(!sources.is_empty(), "SDK production sources are required");
-
- let mut findings = Vec::new();
- for path in sources {
- let source = fs::read_to_string(&path).expect("read SDK production source");
- for (line_index, line) in source.lines().enumerate() {
- if line.trim_start().starts_with("//") {
- continue;
- }
- for retired in RETIRED_IDENTIFIERS {
- if contains_identifier(line, retired) {
- findings.push(format!(
- "{}:{} restores retired identity identifier `{retired}`",
- relative_path(&workspace_root, &path),
- line_index + 1,
- ));
- }
- }
- if line.contains("identity-storage") {
- findings.push(format!(
- "{}:{} restores retired identity-storage feature",
- relative_path(&workspace_root, &path),
- line_index + 1,
- ));
- }
- }
- }
-
- assert!(
- findings.is_empty(),
- "retired SDK identity surface violations:\n{}",
- findings.join("\n")
- );
-}
-
-fn workspace_root() -> PathBuf {
- Path::new(env!("CARGO_MANIFEST_DIR"))
- .parent()
- .and_then(Path::parent)
- .expect("SDK workspace root")
- .to_path_buf()
-}
-
-fn collect_production_sources(directory: &Path, paths: &mut Vec<PathBuf>) {
- if !directory.exists() {
- return;
- }
- for entry in fs::read_dir(directory).expect("read SDK source directory") {
- let path = entry.expect("SDK source entry").path();
- if path.is_dir() {
- if path.file_name().and_then(|name| name.to_str()) != Some("target") {
- collect_production_sources(&path, paths);
- }
- continue;
- }
-
- let in_production_root = path
- .components()
- .any(|component| matches!(component.as_os_str().to_str(), Some("src" | "examples")));
- let supported_extension = matches!(
- path.extension().and_then(|extension| extension.to_str()),
- Some("rs" | "ts" | "js")
- );
- if in_production_root && supported_extension
- || path.file_name().and_then(|name| name.to_str()) == Some("Cargo.toml")
- {
- paths.push(path);
- }
- }
-}
-
-fn contains_identifier(source: &str, identifier: &str) -> bool {
- source.match_indices(identifier).any(|(index, _)| {
- let before = source[..index].chars().next_back();
- let after = source[index + identifier.len()..].chars().next();
- before.is_none_or(|character| !is_identifier_character(character))
- && after.is_none_or(|character| !is_identifier_character(character))
- })
-}
-
-fn is_identifier_character(character: char) -> bool {
- character == '_' || character.is_ascii_alphanumeric()
-}
-
-fn relative_path(root: &Path, path: &Path) -> String {
- path.strip_prefix(root)
- .expect("source path is under workspace root")
- .to_string_lossy()
- .replace('\\', "/")
-}
diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs
@@ -1,783 +0,0 @@
-#![cfg(feature = "knowledge")]
-
-use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
-use radroots_event::envelope::EventEnvelopeParts;
-use radroots_identity::PublicKey;
-use radroots_sdk::knowledge::prelude::*;
-use std::sync::LazyLock;
-
-static KNOWLEDGE_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
-const CREATED_AT: u32 = 1_800_000_000;
-const RELAY: &str = "wss://relay.radroots.example";
-
-#[test]
-fn knowledge_prelude_builds_mvp_wire_parts_without_codec_imports() {
- let article = article_builder().build_event().expect("article");
- let redirect = redirect_builder().build_event().expect("redirect");
- let merge_request = merge_request_builder()
- .build_event()
- .expect("merge request");
- let source = source_builder().build_event().expect("source");
- let claim = claim_builder().build_event().expect("claim");
- let relation = relation_builder().build_event().expect("relation");
- let review = review_builder().build_event().expect("review");
- let field_report = field_report_builder().build_event().expect("field report");
-
- assert_eq!(article.kind, KIND_WIKI_ARTICLE);
- assert_eq!(redirect.kind, KIND_WIKI_REDIRECT);
- assert_eq!(merge_request.kind, KIND_WIKI_MERGE_REQUEST);
- assert_eq!(source.kind, KIND_KNOWLEDGE_SOURCE);
- assert_eq!(claim.kind, KIND_KNOWLEDGE_CLAIM);
- assert_eq!(relation.kind, KIND_KNOWLEDGE_RELATION);
- assert_eq!(review.kind, KIND_KNOWLEDGE_REVIEW);
- assert_eq!(field_report.kind, KIND_KNOWLEDGE_FIELD_REPORT);
-}
-
-#[test]
-fn fluent_builders_auto_fill_schema_and_prepare_drafts() {
- let article = article_builder().build().expect("article");
- let redirect = redirect_builder().build().expect("redirect");
- let merge_request = merge_request_builder().build().expect("merge request");
- let source = source_builder().build().expect("source");
- let claim = claim_builder().build().expect("claim");
- let relation = relation_builder().build().expect("relation");
- let review = review_builder().build().expect("review");
- let field_report = field_report_builder().build().expect("field report");
-
- assert_eq!(article.d_tag, "soil-health");
- assert_eq!(redirect.target, address_ref());
- assert_eq!(
- merge_request.explanation.as_deref(),
- Some("Merge synthetic soil article updates")
- );
- assert_eq!(source.schema, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA);
- assert_eq!(claim.schema, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA);
- assert_eq!(relation.schema, RADROOTS_KNOWLEDGE_RELATION_SCHEMA);
- assert_eq!(review.schema, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA);
- assert_eq!(field_report.schema, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA);
- assert_eq!(claim.schema_version, RADROOTS_KNOWLEDGE_SCHEMA_VERSION);
-
- let article_draft = article_builder()
- .build_draft(public_key_hex(), CREATED_AT)
- .expect("article draft");
- let claim_draft = claim_builder()
- .build_draft(public_key_hex(), CREATED_AT)
- .expect("claim draft");
- assert_eq!(article_draft.contract_id(), WIKI_ARTICLE_CONTRACT_ID);
- assert_eq!(claim_draft.contract_id(), KNOWLEDGE_CLAIM_CONTRACT_ID);
-}
-
-#[test]
-fn fluent_builders_reject_missing_and_invalid_required_fields() {
- let missing_text = RadrootsKnowledgeClaimBuilder::new()
- .claim_type("practice_effect")
- .build()
- .expect_err("missing text");
- assert_eq!(
- missing_text,
- RadrootsKnowledgeBuilderError::MissingField("text")
- );
-
- let invalid_d_tag = RadrootsWikiArticleBuilder::new("Soil Health")
- .title("Soil health")
- .content_djot("# Soil health")
- .build()
- .expect_err("invalid d tag");
- assert_eq!(
- invalid_d_tag,
- RadrootsKnowledgeBuilderError::InvalidField("d_tag")
- );
-}
-
-#[test]
-fn fluent_builders_reject_core_invalid_nested_models() {
- let mut non_wiki_target = address_ref();
- non_wiki_target.kind = KIND_KNOWLEDGE_SOURCE;
- let redirect_error = RadrootsWikiRedirectBuilder::new("soil")
- .target(non_wiki_target)
- .build()
- .expect_err("non wiki redirect target");
- assert_eq!(
- redirect_error,
- RadrootsKnowledgeBuilderError::InvalidField("wiki_redirect.target")
- );
-
- let destination_error = merge_request_builder()
- .destination_pubkey("bad")
- .build()
- .expect_err("invalid destination pubkey");
- assert_eq!(
- destination_error,
- RadrootsKnowledgeBuilderError::InvalidField("destination_pubkey")
- );
-
- let source_version_error = merge_request_builder()
- .source_version_event_id("bad")
- .build()
- .expect_err("invalid source version id");
- assert_eq!(
- source_version_error,
- RadrootsKnowledgeBuilderError::InvalidField("source_version_event_id")
- );
-
- let blank_author_error = source_builder()
- .author(" ")
- .build()
- .expect_err("blank author");
- assert_eq!(
- blank_author_error,
- RadrootsKnowledgeBuilderError::MissingField("authors")
- );
-
- let malformed_artifact_error = source_builder()
- .artifact_ref(malformed_event_ref(KIND_FILE_METADATA))
- .build()
- .expect_err("malformed artifact ref");
- assert_eq!(
- malformed_artifact_error,
- RadrootsKnowledgeBuilderError::InvalidField("artifact_refs")
- );
-
- let mut invalid_subject = knowledge_node_ref("cover crops");
- invalid_subject.external_id = Some("duplicate".to_owned());
- let invalid_node_error = RadrootsKnowledgeRelationBuilder::new()
- .subject(invalid_subject)
- .predicate("supports")
- .object(knowledge_node_ref("soil structure"))
- .support_ref(event_ref('7', KIND_KNOWLEDGE_CLAIM))
- .build()
- .expect_err("invalid relation node");
- assert_eq!(
- invalid_node_error,
- RadrootsKnowledgeBuilderError::InvalidField("subject")
- );
-
- let malformed_support_error = relation_builder()
- .support_ref(malformed_event_ref(KIND_KNOWLEDGE_CLAIM))
- .build()
- .expect_err("malformed support ref");
- assert_eq!(
- malformed_support_error,
- RadrootsKnowledgeBuilderError::InvalidField("support_refs")
- );
-
- let zero_kind_target = KnowledgeReviewTarget {
- event_id: hex_64('8'),
- author_pubkey: hex_64('a'),
- kind: 0,
- address: None,
- relays: vec![RELAY.to_owned()],
- review_scope: KnowledgeReviewScope::SpecificVersion,
- };
- let zero_kind_error = review_builder()
- .target(zero_kind_target)
- .build()
- .expect_err("zero target kind");
- assert_eq!(
- zero_kind_error,
- RadrootsKnowledgeBuilderError::InvalidField("review_target")
- );
-
- let malformed_pubkey_target = KnowledgeReviewTarget {
- event_id: hex_64('8'),
- author_pubkey: "bad".to_owned(),
- kind: KIND_KNOWLEDGE_CLAIM,
- address: None,
- relays: vec![RELAY.to_owned()],
- review_scope: KnowledgeReviewScope::SpecificVersion,
- };
- let malformed_pubkey_error = review_builder()
- .target(malformed_pubkey_target)
- .build()
- .expect_err("malformed target pubkey");
- assert_eq!(
- malformed_pubkey_error,
- RadrootsKnowledgeBuilderError::InvalidField("review_target")
- );
-
- let blank_score_dimension_error = review_builder()
- .score(KnowledgeReviewScore {
- dimension: " ".to_owned(),
- value: "partial".to_owned(),
- note: None,
- })
- .build()
- .expect_err("blank score dimension");
- assert_eq!(
- blank_score_dimension_error,
- RadrootsKnowledgeBuilderError::MissingField("scores")
- );
-
- let exact_private_without_ref_error = field_report_builder()
- .context(KnowledgeFieldContext {
- location_precision: KnowledgeLocationPrecision::ExactPrivateReference,
- public_location: None,
- private_location_ref: None,
- topics: vec!["field".to_owned()],
- context_tags: vec!["observation".to_owned()],
- })
- .build()
- .expect_err("missing private location ref");
- assert_eq!(
- exact_private_without_ref_error,
- RadrootsKnowledgeBuilderError::MissingField("private_location_ref")
- );
-
- let blank_observation_error = field_report_builder()
- .observation(KnowledgeObservation {
- observation_type: " ".to_owned(),
- text: "Residue was visible across beds.".to_owned(),
- observed_at: Some("2026-07-05".to_owned()),
- values: Vec::new(),
- })
- .build()
- .expect_err("blank observation data");
- assert_eq!(
- blank_observation_error,
- RadrootsKnowledgeBuilderError::MissingField("observations")
- );
-}
-
-#[test]
-fn wiki_article_builder_accepts_missing_title_but_rejects_blank_title() {
- let article = RadrootsWikiArticleBuilder::new("soil-health")
- .content_djot("# Soil health")
- .build()
- .expect("title-free article");
- assert_eq!(article.title, None);
-
- let parts = RadrootsWikiArticleBuilder::new("soil-health")
- .content_djot("# Soil health")
- .build_event()
- .expect("title-free article parts");
- assert!(
- !parts
- .tags
- .iter()
- .any(|tag| tag.first().map(String::as_str) == Some("title"))
- );
-
- let blank_title = RadrootsWikiArticleBuilder::new("soil-health")
- .title(" ")
- .content_djot("# Soil health")
- .build()
- .expect_err("blank title");
- assert_eq!(
- blank_title,
- RadrootsKnowledgeBuilderError::MissingField("title")
- );
-}
-
-#[test]
-fn knowledge_claim_builder_enforces_citation_rules() {
- let missing_citations = RadrootsKnowledgeClaimBuilder::new()
- .claim_type("practice_effect")
- .text("Cover crops improve soil structure.")
- .build()
- .expect_err("missing citations");
- assert_eq!(
- missing_citations,
- RadrootsKnowledgeBuilderError::MissingField("citation_spans")
- );
-
- assert!(claim_builder().build().is_ok());
-
- for claim_type in ["hypothesis", "observation", "question"] {
- let claim = RadrootsKnowledgeClaimBuilder::new()
- .claim_type(claim_type)
- .text("Synthetic uncited claim.")
- .build()
- .expect("uncited claim");
- assert_eq!(claim.claim_type, claim_type);
- assert!(claim.citation_spans.is_empty());
- }
-
- let capitalized = RadrootsKnowledgeClaimBuilder::new()
- .claim_type("Hypothesis")
- .text("Synthetic uncited claim.")
- .build()
- .expect_err("capitalized claim type requires citations");
- assert_eq!(
- capitalized,
- RadrootsKnowledgeBuilderError::MissingField("citation_spans")
- );
-}
-
-#[test]
-fn knowledge_draft_builder_freezes_mvp_drafts_without_runtime() {
- let draft_builder = KnowledgeDraftBuilder::new(public_key_hex(), CREATED_AT);
-
- let article = draft_builder
- .wiki_article(&wiki_article())
- .expect("article draft");
- let redirect = draft_builder
- .wiki_redirect(&wiki_redirect())
- .expect("redirect draft");
- let merge_request = draft_builder
- .wiki_merge_request(&wiki_merge_request())
- .expect("merge request draft");
- let source = draft_builder
- .knowledge_source(&knowledge_source())
- .expect("source draft");
- let claim = draft_builder
- .knowledge_claim(&knowledge_claim())
- .expect("claim draft");
- let relation = draft_builder
- .knowledge_relation(&knowledge_relation())
- .expect("relation draft");
- let review = draft_builder
- .knowledge_review(&knowledge_review())
- .expect("review draft");
- let field_report = draft_builder
- .knowledge_field_report(&knowledge_field_report())
- .expect("field report draft");
-
- assert_eq!(article.contract_id(), WIKI_ARTICLE_CONTRACT_ID);
- assert_eq!(redirect.contract_id(), WIKI_REDIRECT_CONTRACT_ID);
- assert_eq!(merge_request.contract_id(), WIKI_MERGE_REQUEST_CONTRACT_ID);
- assert_eq!(source.contract_id(), KNOWLEDGE_SOURCE_CONTRACT_ID);
- assert_eq!(claim.contract_id(), KNOWLEDGE_CLAIM_CONTRACT_ID);
- assert_eq!(relation.contract_id(), KNOWLEDGE_RELATION_CONTRACT_ID);
- assert_eq!(review.contract_id(), KNOWLEDGE_REVIEW_CONTRACT_ID);
- assert_eq!(
- field_report.contract_id(),
- KNOWLEDGE_FIELD_REPORT_CONTRACT_ID
- );
- assert_eq!(claim.expected_pubkey().to_hex(), public_key_hex());
- assert_eq!(claim.created_at_u64(), u64::from(CREATED_AT));
- assert_eq!(claim.kind_u32(), KIND_KNOWLEDGE_CLAIM);
-}
-
-#[test]
-fn knowledge_codec_exposes_manifest_and_verified_decode() {
- let codec = KnowledgeCodec::new();
- let manifest = codec.contract_manifest();
-
- assert_eq!(
- manifest.schema_version,
- RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION
- );
- assert_eq!(manifest.contract_count, 11);
- assert!(
- manifest
- .contracts
- .iter()
- .any(|contract| contract.contract_id == KNOWLEDGE_CLAIM_CONTRACT_ID)
- );
- let claim_contract = manifest
- .contracts
- .iter()
- .find(|contract| contract.contract_id == KNOWLEDGE_CLAIM_CONTRACT_ID)
- .expect("claim manifest contract");
- assert!(claim_contract.sdk_builder_support);
- assert!(claim_contract.sdk_draft_support);
- assert!(claim_contract.wasm_tag_builder_support);
- assert!(claim_contract.wasm_verified_decode_support);
-
- let signed = sign_parts(claim_builder().build_event().expect("claim parts"));
- let decoded = codec
- .verify_and_decode_radroots_event(signed)
- .expect("decoded claim");
-
- match decoded {
- RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
- assert_eq!(parsed.data.data.text, "Cover crops improve soil structure.");
- }
- _ => panic!("expected knowledge claim"),
- }
-
- let sha256 = codec.contract_manifest_sha256().expect("manifest sha256");
- assert_eq!(sha256.len(), 64);
-}
-
-#[test]
-fn knowledge_errors_expose_stable_codes() {
- let mut article = wiki_article();
- article.d_tag = "Soil Health".to_owned();
- let error = build_wiki_article_event(&article).expect_err("invalid d tag");
-
- assert_eq!(error.code(), "knowledge_encode");
- assert_eq!(error.inner_code(), "invalid_field");
- assert!(!error.to_string().contains(article.content_djot.as_str()));
-
- let draft_error: RadrootsSdkKnowledgeError = EventDraft::new(
- KNOWLEDGE_CLAIM_CONTRACT_ID,
- KIND_KNOWLEDGE_CLAIM,
- u64::from(CREATED_AT),
- Vec::new(),
- r#"{"schema":"radroots.knowledge.claim.v1","schema_version":1}"#,
- public_key_hex(),
- )
- .expect_err("contract shape")
- .into();
- assert_eq!(draft_error.code(), "knowledge_draft");
- assert_eq!(draft_error.inner_code(), "missing_tag");
-
- let draft_errors = [
- (
- DraftError::ContractNotDraftAuthorable {
- contract_id: KNOWLEDGE_CLAIM_CONTRACT_ID.to_owned(),
- },
- "contract_not_draft_authorable",
- ),
- (
- DraftError::ContractRegistryVersionMismatch {
- expected: 2,
- actual: 1,
- },
- "contract_registry_version_mismatch",
- ),
- (
- DraftError::DraftExpectedEventIdMismatch {
- expected_event_id: hex_64('a'),
- actual_event_id: hex_64('b'),
- },
- "draft_expected_event_id_mismatch",
- ),
- ];
-
- for (draft_error, expected_inner_code) in draft_errors {
- let error = RadrootsSdkKnowledgeError::from(draft_error);
- assert_eq!(error.code(), "knowledge_draft");
- assert_eq!(error.inner_code(), expected_inner_code);
- }
-}
-
-fn sign_parts(parts: Nip01EventWireParts) -> EventEnvelope {
- let tags = parts
- .tags
- .into_iter()
- .map(Tag::parse)
- .collect::<Result<Vec<_>, _>>()
- .expect("tags");
- let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
- .tags(tags)
- .custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
- .sign_with_keys(&KNOWLEDGE_KEYS)
- .expect("signed event");
- EventEnvelope::new(EventEnvelopeParts {
- id: event.id.to_hex(),
- author: event.pubkey.to_hex(),
- created_at: event.created_at.as_secs(),
- kind: u32::from(event.kind.as_u16()),
- tags: event
- .tags
- .as_slice()
- .iter()
- .map(|tag| tag.as_slice().to_vec())
- .collect(),
- content: event.content,
- sig: event.sig.to_string(),
- })
- .expect("event envelope")
-}
-
-fn public_key_hex() -> String {
- KNOWLEDGE_KEYS.public_key().to_hex()
-}
-
-fn hex_64(character: char) -> String {
- character.to_string().repeat(64)
-}
-
-fn event_ref(character: char, kind: u32) -> EventRef {
- EventRef {
- id: hex_64(character),
- author: PublicKey::from_hex(&public_key_hex()).expect("fixture public key"),
- kind,
- d_tag: None,
- relays: Some(vec![RELAY.to_owned()]),
- }
-}
-
-fn malformed_event_ref(kind: u32) -> EventRef {
- let mut reference = event_ref('f', kind);
- reference.id = "bad".to_owned();
- reference
-}
-
-fn address_ref() -> AddressableRef {
- AddressableRef {
- kind: KIND_WIKI_ARTICLE,
- pubkey: hex_64('a'),
- d_tag: "soil-health".to_owned(),
- relays: vec![RELAY.to_owned()],
- }
-}
-
-fn wiki_article() -> WikiArticle {
- WikiArticle {
- d_tag: "soil-health".to_owned(),
- title: Some("Soil health".to_owned()),
- content_djot: "# Soil health".to_owned(),
- summary: Some("Living soil basics".to_owned()),
- topics: vec!["soil".to_owned(), "local-food".to_owned()],
- references: vec![event_ref('1', KIND_KNOWLEDGE_SOURCE)],
- forked_from: Vec::new(),
- deferred_to: None,
- }
-}
-
-fn wiki_redirect() -> WikiRedirect {
- WikiRedirect {
- d_tag: "soil".to_owned(),
- target: address_ref(),
- }
-}
-
-fn article_version_ref() -> WikiArticleVersionRef {
- WikiArticleVersionRef {
- event_id: hex_64('b'),
- address_ref: address_ref(),
- }
-}
-
-fn wiki_merge_request() -> WikiMergeRequest {
- WikiMergeRequest {
- target_article: address_ref(),
- destination_pubkey: hex_64('a'),
- base_version_event_id: Some(hex_64('e')),
- source_version_event_id: hex_64('f'),
- explanation: Some("Merge synthetic soil article updates".to_owned()),
- }
-}
-
-fn article_builder() -> RadrootsWikiArticleBuilder {
- RadrootsWikiArticleBuilder::new("soil-health")
- .title("Soil health")
- .content_djot("# Soil health")
- .summary("Living soil basics")
- .topic("soil")
- .topic("local-food")
- .reference(event_ref('1', KIND_KNOWLEDGE_SOURCE))
- .forked_from(article_version_ref())
-}
-
-fn redirect_builder() -> RadrootsWikiRedirectBuilder {
- RadrootsWikiRedirectBuilder::new("soil").target(address_ref())
-}
-
-fn merge_request_builder() -> RadrootsWikiMergeRequestBuilder {
- RadrootsWikiMergeRequestBuilder::new()
- .target_article(address_ref())
- .destination_pubkey(hex_64('a'))
- .base_version_event_id(hex_64('e'))
- .source_version_event_id(hex_64('f'))
- .explanation("Merge synthetic soil article updates")
-}
-
-fn source_builder() -> RadrootsKnowledgeSourceBuilder {
- RadrootsKnowledgeSourceBuilder::new("soil-source")
- .title("Soil Source")
- .source_type("book")
- .author("A. Example")
- .publisher("Radroots Synthetic Press")
- .publication_year(2026)
- .canonical_url("https://source.example.test/soil-source")
- .artifact_ref(event_ref('3', KIND_FILE_METADATA))
- .topic("soil")
- .summary("Synthetic source for SDK coverage")
-}
-
-fn claim_builder() -> RadrootsKnowledgeClaimBuilder {
- RadrootsKnowledgeClaimBuilder::new()
- .claim_type("practice_effect")
- .text("Cover crops improve soil structure.")
- .citation_span(KnowledgeCitationSpan {
- source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE),
- artifact_ref: None,
- page_start: Some(12),
- page_end: Some(13),
- section_path: vec!["chapter-1".to_owned()],
- quote_hash: Some(hex_64('5')),
- chunk_id: Some("chunk-1".to_owned()),
- })
- .topic("cover-crops")
- .applies_to("local-food")
- .author_asserted_confidence("medium")
-}
-
-fn relation_builder() -> RadrootsKnowledgeRelationBuilder {
- RadrootsKnowledgeRelationBuilder::new()
- .subject(knowledge_node_ref("cover crops"))
- .predicate("supports")
- .object(knowledge_node_ref("soil structure"))
- .support_ref(event_ref('7', KIND_KNOWLEDGE_CLAIM))
- .author_asserted_confidence("medium")
-}
-
-fn review_builder() -> RadrootsKnowledgeReviewBuilder {
- RadrootsKnowledgeReviewBuilder::new()
- .target(KnowledgeReviewTarget {
- event_id: hex_64('8'),
- author_pubkey: hex_64('a'),
- kind: KIND_KNOWLEDGE_CLAIM,
- address: None,
- relays: vec![RELAY.to_owned()],
- review_scope: KnowledgeReviewScope::SpecificVersion,
- })
- .reviewer_role("peer")
- .verdict("needs_more_evidence")
- .score(KnowledgeReviewScore {
- dimension: "evidence".to_owned(),
- value: "partial".to_owned(),
- note: None,
- })
- .notes("Synthetic review")
- .evidence_ref(event_ref('9', KIND_KNOWLEDGE_SOURCE))
-}
-
-fn field_report_builder() -> RadrootsKnowledgeFieldReportBuilder {
- RadrootsKnowledgeFieldReportBuilder::new()
- .report_type("observation")
- .title("Field observation")
- .summary("Observed cover crop residue.")
- .context(KnowledgeFieldContext {
- location_precision: KnowledgeLocationPrecision::CoarseGeohash,
- public_location: Some(KnowledgeLocation {
- label: Some("watershed".to_owned()),
- region: Some("synthetic-region".to_owned()),
- locality: None,
- geohash: Some("c23".to_owned()),
- }),
- private_location_ref: None,
- topics: vec!["field".to_owned()],
- context_tags: vec!["observation".to_owned()],
- })
- .observation(KnowledgeObservation {
- observation_type: "residue".to_owned(),
- text: "Residue was visible across beds.".to_owned(),
- observed_at: Some("2026-07-05".to_owned()),
- values: vec![KnowledgeObservationValue {
- key: "coverage".to_owned(),
- value: "medium".to_owned(),
- unit: None,
- }],
- })
- .artifact_ref(event_ref('c', KIND_FILE_METADATA))
- .related_ref(event_ref('d', KIND_KNOWLEDGE_CLAIM))
- .limitation("single observer")
-}
-
-fn knowledge_source() -> KnowledgeSource {
- KnowledgeSource {
- schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_owned(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- d_tag: "soil-source".to_owned(),
- title: "Soil Source".to_owned(),
- source_type: "book".to_owned(),
- authors: vec!["A. Example".to_owned()],
- publisher: Some("Radroots Synthetic Press".to_owned()),
- publication_year: Some(2026),
- edition: None,
- canonical_url: Some("https://source.example.test/soil-source".to_owned()),
- artifact_refs: vec![event_ref('3', KIND_FILE_METADATA)],
- author_asserted_rights: None,
- topics: vec!["soil".to_owned()],
- summary: Some("Synthetic source for SDK coverage".to_owned()),
- }
-}
-
-fn knowledge_claim() -> KnowledgeClaim {
- KnowledgeClaim {
- schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_owned(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- claim_type: "practice_effect".to_owned(),
- text: "Cover crops improve soil structure.".to_owned(),
- citation_spans: vec![KnowledgeCitationSpan {
- source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE),
- artifact_ref: None,
- page_start: Some(12),
- page_end: Some(13),
- section_path: vec!["chapter-1".to_owned()],
- quote_hash: Some(hex_64('5')),
- chunk_id: Some("chunk-1".to_owned()),
- }],
- topics: vec!["cover-crops".to_owned()],
- applies_to: vec!["local-food".to_owned()],
- author_asserted_confidence: Some("medium".to_owned()),
- supersedes: Vec::new(),
- }
-}
-
-fn knowledge_node_ref(label: &str) -> KnowledgeNodeRef {
- KnowledgeNodeRef {
- node_type: "event".to_owned(),
- event_ref: Some(event_ref('6', KIND_KNOWLEDGE_CLAIM)),
- address_ref: None,
- external_id: None,
- label: Some(label.to_owned()),
- }
-}
-
-fn knowledge_relation() -> KnowledgeRelation {
- KnowledgeRelation {
- schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_owned(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- subject: knowledge_node_ref("cover crops"),
- predicate: "supports".to_owned(),
- object: knowledge_node_ref("soil structure"),
- support_refs: vec![event_ref('7', KIND_KNOWLEDGE_CLAIM)],
- author_asserted_confidence: Some("medium".to_owned()),
- supersedes: Vec::new(),
- }
-}
-
-fn knowledge_review() -> KnowledgeReview {
- KnowledgeReview {
- schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_owned(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- target: KnowledgeReviewTarget {
- event_id: hex_64('8'),
- author_pubkey: hex_64('a'),
- kind: KIND_KNOWLEDGE_CLAIM,
- address: None,
- relays: vec![RELAY.to_owned()],
- review_scope: KnowledgeReviewScope::SpecificVersion,
- },
- reviewer_role: "peer".to_owned(),
- verdict: "needs_more_evidence".to_owned(),
- scores: vec![KnowledgeReviewScore {
- dimension: "evidence".to_owned(),
- value: "partial".to_owned(),
- note: None,
- }],
- notes: Some("Synthetic review".to_owned()),
- evidence_refs: vec![event_ref('9', KIND_KNOWLEDGE_SOURCE)],
- }
-}
-
-fn knowledge_field_report() -> KnowledgeFieldReport {
- KnowledgeFieldReport {
- schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_owned(),
- schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
- report_type: "observation".to_owned(),
- title: "Field observation".to_owned(),
- summary: Some("Observed cover crop residue.".to_owned()),
- context: KnowledgeFieldContext {
- location_precision: KnowledgeLocationPrecision::CoarseGeohash,
- public_location: Some(KnowledgeLocation {
- label: Some("watershed".to_owned()),
- region: Some("synthetic-region".to_owned()),
- locality: None,
- geohash: Some("c23".to_owned()),
- }),
- private_location_ref: None,
- topics: vec!["field".to_owned()],
- context_tags: vec!["observation".to_owned()],
- },
- observations: vec![KnowledgeObservation {
- observation_type: "residue".to_owned(),
- text: "Residue was visible across beds.".to_owned(),
- observed_at: Some("2026-07-05".to_owned()),
- values: vec![KnowledgeObservationValue {
- key: "coverage".to_owned(),
- value: "medium".to_owned(),
- unit: None,
- }],
- }],
- artifact_refs: vec![event_ref('c', KIND_FILE_METADATA)],
- related_refs: vec![event_ref('d', KIND_KNOWLEDGE_CLAIM)],
- limitations: vec!["single observer".to_owned()],
- }
-}
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -12,6 +12,7 @@ const ADAPTERS: &str = include_str!("../src/adapters/mod.rs");
const RADROOTSD: &str = include_str!("../src/adapters/radrootsd.rs");
const SYNC: &str = include_str!("../src/sync.rs");
const TRANSPORT: &str = include_str!("../src/transport.rs");
+const PUBLICATION: &str = include_str!("../../../contracts/releases/publication.toml");
#[test]
fn manifest_has_final_identity_and_dependency_boundary() {
@@ -143,6 +144,59 @@ fn root_declares_exact_final_module_skeleton() {
}
#[test]
+fn package_contains_only_reachable_sources_and_registered_targets() {
+ let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+ assert_eq!(
+ rust_files(&root.join("src")),
+ BTreeSet::from([
+ "adapters/mod.rs".to_owned(),
+ "adapters/radrootsd.rs".to_owned(),
+ "capability.rs".to_owned(),
+ "client.rs".to_owned(),
+ "diagnostics.rs".to_owned(),
+ "error.rs".to_owned(),
+ "farm.rs".to_owned(),
+ "lib.rs".to_owned(),
+ "listing.rs".to_owned(),
+ "signing.rs".to_owned(),
+ "storage.rs".to_owned(),
+ "sync.rs".to_owned(),
+ "trade.rs".to_owned(),
+ "transport.rs".to_owned(),
+ ])
+ );
+ assert_eq!(
+ rust_files(&root.join("tests")),
+ BTreeSet::from([
+ "lifecycle.rs".to_owned(),
+ "package_boundary.rs".to_owned(),
+ "public_api.rs".to_owned(),
+ "unit/adapters_radrootsd_tests.rs".to_owned(),
+ ])
+ );
+ assert_eq!(
+ rust_files(&root.join("examples")),
+ BTreeSet::from([
+ "safe_memory_client.rs".to_owned(),
+ "transport_profile.rs".to_owned(),
+ ])
+ );
+}
+
+#[test]
+fn remaining_compatibility_package_is_retired_and_not_publishable() {
+ assert!(PUBLICATION.contains("retired = [\"radroots_runtime_contract_v1\"]"));
+ let approved = PUBLICATION
+ .split_once("approved_packages = [")
+ .expect("approved package list")
+ .1
+ .split_once("\n]")
+ .expect("approved package list terminator")
+ .0;
+ assert!(!approved.contains("radroots_runtime_contract_v1"));
+}
+
+#[test]
fn root_types_have_the_required_std_contracts() {
fn assert_client<T: Clone + Send + Sync>() {}
fn assert_builder<T: Send + Sync>() {}
@@ -445,3 +499,24 @@ fn dependency_names(manifest: &str) -> BTreeSet<&str> {
.filter(|name| name.starts_with("radroots_"))
.collect()
}
+
+fn rust_files(root: &std::path::Path) -> BTreeSet<String> {
+ let mut files = BTreeSet::new();
+ let mut pending = vec![root.to_path_buf()];
+ while let Some(directory) = pending.pop() {
+ for entry in std::fs::read_dir(&directory).expect("read package source directory") {
+ let path = entry.expect("read package source entry").path();
+ if path.is_dir() {
+ pending.push(path);
+ } else if path.extension().and_then(|value| value.to_str()) == Some("rs") {
+ files.insert(
+ path.strip_prefix(root)
+ .expect("source remains below root")
+ .to_string_lossy()
+ .replace(std::path::MAIN_SEPARATOR, "/"),
+ );
+ }
+ }
+ }
+ files
+}
diff --git a/crates/sdk/tests/replica_ingest.rs b/crates/sdk/tests/replica_ingest.rs
@@ -1,89 +0,0 @@
-use radroots_event::{
- envelope::{EventEnvelope, EventEnvelopeParts},
- farm::Farm,
-};
-use radroots_replica_schema::farm::IFarmFindMany;
-use radroots_replica_store::ReplicaSql;
-use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
-use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor};
-use tempfile::{TempDir, tempdir};
-
-fn seller_pubkey() -> String {
- "a".repeat(64)
-}
-
-fn sdk_event(
- id: u64,
- author: &str,
- created_at: u32,
- kind: u32,
- content: String,
- tags: Vec<Vec<String>>,
-) -> EventEnvelope {
- EventEnvelope::new(EventEnvelopeParts {
- id: format!("{id:064x}"),
- author: author.to_owned(),
- created_at: u64::from(created_at),
- kind,
- tags,
- content,
- sig: "f".repeat(128),
- })
- .expect("sdk event envelope")
-}
-
-fn sample_farm() -> Farm {
- Farm {
- d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(),
- name: "North Farm".into(),
- about: Some("Organic coffee".into()),
- website: None,
- picture: None,
- banner: None,
- location: None,
- tags: Some(vec!["coffee".into()]),
- }
-}
-
-fn open_replica() -> (TempDir, ReplicaSql<SqlxSqliteExecutor>) {
- let dir = tempdir().expect("tempdir");
- let db_path = dir.path().join("replica.sqlite");
- let executor = SqlxSqliteExecutor::open(&db_path).expect("open sqlite");
- executor
- .exec("PRAGMA foreign_keys = ON;", "[]")
- .expect("enable foreign keys");
- let replica = ReplicaSql::new(executor);
- replica.migrate_up().expect("migrate");
- (dir, replica)
-}
-
-fn ingest_farm(replica: &ReplicaSql<SqlxSqliteExecutor>) -> EventEnvelope {
- let farm_value = sample_farm();
- let author = seller_pubkey();
- let parts = radroots_event_codec::encode::farm::to_wire_parts(&farm_value).expect("farm draft");
- let event = sdk_event(
- 1,
- &author,
- 1_720_000_000,
- parts.kind,
- parts.content,
- parts.tags,
- );
- let outcome = radroots_replica_ingest_event(replica.executor(), &event).expect("ingest farm");
- assert_eq!(outcome, RadrootsReplicaIngestOutcome::Applied);
- event
-}
-
-#[test]
-fn sdk_farm_draft_ingests_into_replica_projection() {
- let (_dir, replica) = open_replica();
- let event = ingest_farm(&replica);
- let farms = replica
- .farm_find_many(&IFarmFindMany { filter: None })
- .expect("query farms")
- .results;
- assert_eq!(farms.len(), 1);
- assert_eq!(farms[0].d_tag, sample_farm().d_tag);
- assert_eq!(farms[0].name, sample_farm().name);
- assert_eq!(farms[0].pubkey, event.author().to_hex());
-}
diff --git a/crates/sdk/tests/secrets_migration_boundary.rs b/crates/sdk/tests/secrets_migration_boundary.rs
@@ -1,62 +0,0 @@
-use std::fs;
-use std::path::Path;
-
-const ROOT_MANIFEST: &str = include_str!("../../../Cargo.toml");
-const PACKAGE_MANIFEST: &str = include_str!("../Cargo.toml");
-const CARGO_CONFIG: &str = include_str!("../../../.cargo/config.toml");
-const PRIVATE_STORE: &str = include_str!("../src/private_store.rs");
-const DEVIATIONS: &str = include_str!("../../../docs/implementation/deviations.toml");
-
-#[test]
-fn final_secret_dependency_is_activated_at_the_sdk_boundary() {
- assert!(ROOT_MANIFEST.contains(
- "radroots_secrets = { package = \"radroots_secrets\", version = \"=0.1.0-alpha\", default-features = false }"
- ));
- assert!(PACKAGE_MANIFEST.contains(
- "radroots_secrets = { workspace = true, optional = true, default-features = false }"
- ));
- assert!(PACKAGE_MANIFEST.contains("\"dep:radroots_secrets\""));
- assert!(CARGO_CONFIG.contains("radroots_secrets = { path = \"../lib/crates/secrets\" }"));
-}
-
-#[test]
-fn predecessor_secret_imports_are_confined_to_the_private_store_quarantine() {
- let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
- let mut legacy_sources = Vec::new();
- collect_rust_sources(&source_root, &mut legacy_sources);
- legacy_sources.retain(|path| {
- let source = fs::read_to_string(path).expect("read SDK source");
- source.contains("radroots_protected_store") || source.contains("radroots_secret_vault")
- });
-
- assert_eq!(legacy_sources, vec![source_root.join("private_store.rs")]);
- assert!(PRIVATE_STORE.contains("RCRV1-DEV-008"));
- assert!(PRIVATE_STORE.contains("Step 179 transfers the private store"));
-}
-
-#[test]
-fn quarantine_has_exact_future_removal_gates() {
- for required in [
- "id = \"RCRV1-DEV-008\"",
- "affected_steps = [\"153\", \"155\", \"171\", \"179\", \"226\", \"288\", \"293\", \"313\"]",
- "Step 179 transfers canonical private storage",
- "Step 313 removes every remaining compatibility package and legacy name",
- ] {
- assert!(
- DEVIATIONS.contains(required),
- "secret consumer quarantine is missing `{required}`"
- );
- }
-}
-
-fn collect_rust_sources(root: &Path, sources: &mut Vec<std::path::PathBuf>) {
- for entry in fs::read_dir(root).expect("read SDK source directory") {
- let path = entry.expect("SDK source entry").path();
- if path.is_dir() {
- collect_rust_sources(&path, sources);
- } else if path.extension().is_some_and(|extension| extension == "rs") {
- sources.push(path);
- }
- }
- sources.sort();
-}
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -1,326 +0,0 @@
-use std::{
- fs,
- path::{Path, PathBuf},
-};
-
-fn manifest_dir() -> PathBuf {
- PathBuf::from(env!("CARGO_MANIFEST_DIR"))
-}
-
-fn read_source(path: &Path) -> String {
- fs::read_to_string(path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()))
-}
-
-fn active_source_files() -> Vec<PathBuf> {
- fn collect_rust_sources(directory: &Path, files: &mut Vec<PathBuf>) {
- let mut entries = fs::read_dir(directory)
- .unwrap_or_else(|error| panic!("read {}: {error}", directory.display()))
- .map(|entry| entry.expect("source entry").path())
- .collect::<Vec<_>>();
- entries.sort();
- for path in entries {
- if path.is_dir() {
- collect_rust_sources(&path, files);
- } else if path.extension().is_some_and(|extension| extension == "rs") {
- files.push(path);
- }
- }
- }
-
- let mut files = Vec::new();
- collect_rust_sources(&manifest_dir().join("src"), &mut files);
- files
-}
-
-#[test]
-fn retired_order_runtime_roots_are_not_active() {
- let manifest = manifest_dir();
- let lib = read_source(&manifest.join("src/lib.rs"));
-
- for forbidden in [
- "mod orders_runtime;",
- "mod market_runtime;",
- "mod trade_storage;",
- "mod order;",
- "pub use crate::orders_runtime",
- "pub use crate::market_runtime",
- "pub use crate::trade_storage",
- ] {
- assert!(
- !lib.contains(forbidden),
- "src/lib.rs must not activate retired runtime root `{forbidden}`"
- );
- }
-
- for retired in [
- "src/orders_runtime.rs",
- "src/market_runtime.rs",
- "src/trade_storage.rs",
- "src/order.rs",
- "tests/orders_runtime.rs",
- "tests/market_runtime.rs",
- "tests/trade_public_api.rs",
- "tests/trade_product_publish_runtime.rs",
- ] {
- assert!(
- !manifest.join(retired).exists(),
- "retired SDK order runtime file must be removed: {retired}"
- );
- }
-}
-
-#[test]
-fn active_sources_do_not_import_retired_trade_modules() {
- for path in active_source_files() {
- let source = read_source(&path);
- for forbidden in [
- "radroots_trade::order",
- "radroots_trade::projection",
- "radroots_trade::model::RadrootsTradeProjectionV1",
- "radroots_trade::reducer::RadrootsTradeReducerIssueV1",
- "radroots_trade::reducer::RadrootsTradeReductionInputV1",
- "radroots_trade::workflow::RadrootsTrade",
- "radroots_trade::RadrootsTradeProjectionV1",
- "radroots_trade::RadrootsTradeReducerIssueV1",
- "radroots_trade::RadrootsTradeReductionInputV1",
- ] {
- assert!(
- !source.contains(forbidden),
- "{} must not import retired trade module `{forbidden}`",
- path.display()
- );
- }
- }
-}
-
-#[test]
-fn active_sources_do_not_import_retired_listing_contracts() {
- for path in active_source_files() {
- let source = read_source(&path);
- for forbidden in [
- "radroots_event::operational_listing",
- "radroots_trade::listing",
- "RadrootsListingAddress",
- "KIND_LISTING",
- ] {
- assert!(
- !source.contains(forbidden),
- "{} must not import retired listing contract `{forbidden}`",
- path.display()
- );
- }
- }
-}
-
-#[test]
-fn active_sources_use_canonical_transport_type_names() {
- for path in active_source_files() {
- let source = read_source(&path);
- for retired in [
- "RadrootsTransportKind",
- "RadrootsTransportMeshScopeId",
- "RadrootsTransportTarget",
- "RadrootsTransportTargetFingerprint",
- "RadrootsTransportTargetLabel",
- "RadrootsTransportTargetSet",
- ] {
- let restores_identifier = source.match_indices(retired).any(|(index, _)| {
- let before = source[..index].chars().next_back();
- let after = source[index + retired.len()..].chars().next();
- before.is_none_or(|character| !(character.is_alphanumeric() || character == '_'))
- && after
- .is_none_or(|character| !(character.is_alphanumeric() || character == '_'))
- });
- assert!(
- !restores_identifier,
- "{} must use the canonical transport type instead of `{retired}`",
- path.display()
- );
- }
- }
-}
-
-#[test]
-fn temporary_transport_mapping_is_publish_frozen_until_step_235() {
- let manifest = manifest_dir();
- let cargo_manifest = read_source(&manifest.join("Cargo.toml"));
- let transport = read_source(&manifest.join("src/transport.rs"));
- let deviations = read_source(&manifest.join("../../docs/implementation/deviations.toml"));
-
- assert!(cargo_manifest.contains("publish = false"));
- for required in [
- "pub enum SatisfactionPolicy",
- "pub struct TargetSet",
- "transport_satisfaction_policy",
- ] {
- assert!(
- transport.contains(required),
- "the bounded Step 235 migration inventory is missing `{required}`"
- );
- }
- for required in [
- "id = \"RCRV1-DEV-007\"",
- "affected_steps = [\"122\", \"170\", \"235\", \"305\"]",
- "SDK-local unpublished target-set and satisfaction-policy mapping only until Step 235",
- ] {
- assert!(
- deviations.contains(required),
- "the Step 235 final-removal record is missing `{required}`"
- );
- }
-}
-
-#[test]
-fn active_sources_do_not_describe_compatibility_paths() {
- for path in active_source_files() {
- let source = read_source(&path).to_lowercase();
- for forbidden in [
- "compatibility",
- "legacy",
- "shim",
- "dual-read",
- "dual-write",
- "fallback adapter",
- ] {
- assert!(
- !source.contains(forbidden),
- "{} must not describe `{forbidden}` behavior",
- path.display()
- );
- }
- }
-}
-
-#[test]
-fn sdk_does_not_expose_generic_wire_part_signing() {
- let manifest = manifest_dir();
- let lib = read_source(&manifest.join("src/lib.rs"));
- let adapters = read_source(&manifest.join("src/adapters/mod.rs"));
-
- assert!(!manifest.join("src/adapters/signing.rs").exists());
- assert!(
- !manifest
- .join("tests/unit/adapters_signing_tests.rs")
- .exists()
- );
- assert!(!lib.contains("feature = \"signing\",\n"));
- assert!(!adapters.contains("pub mod signing"));
-}
-
-#[test]
-fn sdk_consumes_only_the_final_signing_boundary() {
- let manifest = manifest_dir();
- let cargo_manifest = read_source(&manifest.join("Cargo.toml"));
- let workspace = manifest
- .parent()
- .and_then(Path::parent)
- .expect("SDK crate belongs to the workspace root");
- let workspace_manifest = read_source(&workspace.join("Cargo.toml"));
- let cargo_config = read_source(&workspace.join(".cargo/config.toml"));
- assert!(cargo_manifest.contains("radroots_signing = { workspace = true"));
- assert!(workspace_manifest.contains("radroots_signing = { package = \"radroots_signing\""));
- assert!(cargo_config.contains("radroots_signing = { path = \"../lib/crates/signing\" }"));
- for source in [&cargo_manifest, &workspace_manifest, &cargo_config] {
- assert!(!source.contains("radroots_authority"));
- }
-
- let retired_signing_surface = [
- "radroots_authority",
- "RadrootsActorContext",
- "RadrootsEventSigner",
- "RadrootsLocalEventSigner",
- ];
-
- for root in ["src", "tests", "examples"] {
- let directory = manifest.join(root);
- let mut files = Vec::new();
- if directory.exists() {
- fn collect(directory: &Path, files: &mut Vec<PathBuf>) {
- for entry in fs::read_dir(directory).expect("read SDK source tree") {
- let path = entry.expect("SDK source entry").path();
- if path.is_dir() {
- collect(&path, files);
- } else if path.extension().is_some_and(|extension| extension == "rs") {
- files.push(path);
- }
- }
- }
- collect(&directory, &mut files);
- }
- for path in files {
- if path.ends_with("source_boundary.rs") {
- continue;
- }
- let source = read_source(&path);
- for retired in retired_signing_surface {
- assert!(
- !source.contains(retired),
- "{} must use radroots_signing instead of retired `{retired}`",
- path.display()
- );
- }
- }
- }
-}
-
-#[test]
-fn signer_transition_surface_is_private_hidden_and_scheduled_for_removal() {
- let manifest = manifest_dir();
- let package_manifest = read_source(&manifest.join("Cargo.toml"));
- let lib = read_source(&manifest.join("src/lib.rs"));
- let adapters = read_source(&manifest.join("src/adapters/mod.rs"));
- let transition_record =
- read_source(&manifest.join("../../docs/implementation/COMPATIBILITY_SHIMS.md"));
-
- assert!(package_manifest.contains("publish = false"));
- assert!(lib.contains("#[doc(hidden)]\npub use crate::signer_provider::{"));
- assert!(adapters.contains("#[doc(hidden)]\npub mod signer;"));
- assert!(transition_record.contains("SDK signer provider façade"));
- assert!(transition_record.contains("Step 313"));
- assert!(transition_record.contains("oss/cli"));
- assert!(transition_record.contains("oss/studio_app"));
-}
-
-#[test]
-fn signer_consumers_use_the_final_nostr_connect_state_machine() {
- let manifest = manifest_dir();
- for relative in [
- "src/adapters/signer.rs",
- "examples/sdk_v1_myc_nip46_signer_setup.rs",
- ] {
- let source = read_source(&manifest.join(relative));
- for retired in [
- "radroots_nostr_connect::prelude",
- "RadrootsNostrConnectClient",
- "RadrootsNostrConnectMethod",
- "RadrootsNostrConnectPermission",
- "RadrootsNostrConnectRequest",
- "RadrootsNostrConnectResponse",
- "RADROOTS_NOSTR_CONNECT_",
- "RadrootsSdkNip46ClientKey",
- "RadrootsSdkNip46Transport",
- ] {
- assert!(
- !source.contains(retired),
- "{relative} retains retired Nostr Connect surface `{retired}`"
- );
- }
- }
-
- let provider = read_source(&manifest.join("src/signer_provider.rs"));
- assert!(provider.contains("transport: Arc<AsyncMutex<Box<dyn Transport>>>"));
- assert!(provider.contains("impl Transport for RadrootsSdkNip46TimeoutTransport"));
- assert!(provider.contains(".client\n .execute("));
- assert!(provider.contains("pub fn from_client<T>("));
- assert!(provider.contains("CLI migration Step 271; removed in Step 313"));
- for shim in [
- "pub struct RadrootsSdkNip46ClientKey",
- "pub type RadrootsSdkNip46TransportFuture",
- "pub trait RadrootsSdkNip46Transport",
- ] {
- let position = provider
- .find(shim)
- .unwrap_or_else(|| panic!("missing compatibility shim `{shim}`"));
- assert!(provider[..position].ends_with("#[doc(hidden)]\n"));
- }
-}
diff --git a/crates/sdk/tests/support/fixture_signer.rs b/crates/sdk/tests/support/fixture_signer.rs
@@ -1,69 +0,0 @@
-use nostr::Keys as RadrootsNostrKeys;
-use radroots_nostr::signing::sign_frozen_draft;
-use radroots_signing::{
- Error, SignReceipt, SignRequest, Signer, SignerStatus, error::Kind, signer::BoxFuture,
-};
-use std::sync::LazyLock;
-
-struct FixtureKeyMaterial {
- keys: RadrootsNostrKeys,
- pubkey: String,
-}
-
-impl FixtureKeyMaterial {
- fn generate() -> Self {
- let keys = RadrootsNostrKeys::generate();
- let pubkey = keys.public_key().to_hex();
- Self { keys, pubkey }
- }
-}
-
-static FIXTURE_ALICE: LazyLock<FixtureKeyMaterial> = LazyLock::new(FixtureKeyMaterial::generate);
-static FIXTURE_BOB: LazyLock<FixtureKeyMaterial> = LazyLock::new(FixtureKeyMaterial::generate);
-
-pub(crate) fn fixture_alice_pubkey() -> &'static str {
- FIXTURE_ALICE.pubkey.as_str()
-}
-
-pub(crate) fn fixture_bob_pubkey() -> &'static str {
- FIXTURE_BOB.pubkey.as_str()
-}
-
-pub struct FixtureSigner {
- keys: RadrootsNostrKeys,
-}
-
-impl FixtureSigner {
- pub fn new(pubkey: &str) -> Self {
- let material = match pubkey {
- pubkey if pubkey == fixture_alice_pubkey() => &*FIXTURE_ALICE,
- pubkey if pubkey == fixture_bob_pubkey() => &*FIXTURE_BOB,
- _ => panic!("unsupported fixture signer public key"),
- };
- Self {
- keys: material.keys.clone(),
- }
- }
-
- #[allow(dead_code)]
- pub fn sign_frozen_draft(
- &self,
- draft: &radroots_event::EventDraft,
- ) -> Result<radroots_event::SignedEvent, radroots_nostr::Error> {
- sign_frozen_draft(&self.keys, draft)
- }
-}
-
-impl Signer for FixtureSigner {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, Error>> {
- Box::pin(async { Ok(SignerStatus::unavailable()) })
- }
-
- fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> {
- Box::pin(async move {
- let signed_event = sign_frozen_draft(&self.keys, request.draft())
- .map_err(|source| Error::with_source(Kind::AuthorizationDenied, source))?;
- SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001)
- })
- }
-}
diff --git a/crates/sdk/tests/support/serializer_failure.rs b/crates/sdk/tests/support/serializer_failure.rs
@@ -1,274 +0,0 @@
-use serde::Serialize;
-use serde::ser::{self, SerializeStruct};
-
-#[derive(Clone, Copy)]
-enum FailingSerializeFailure {
- Start,
- Field(usize),
- End,
-}
-
-struct FailingStructSerializer {
- failure: FailingSerializeFailure,
-}
-
-struct FailingSerializeStruct {
- field_index: usize,
- failure: FailingSerializeFailure,
-}
-
-#[derive(Debug)]
-struct FailingSerializeError;
-
-impl core::fmt::Display for FailingSerializeError {
- fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- formatter.write_str("intentional serializer failure")
- }
-}
-
-impl std::error::Error for FailingSerializeError {}
-
-impl ser::Error for FailingSerializeError {
- fn custom<T>(_message: T) -> Self
- where
- T: core::fmt::Display,
- {
- Self
- }
-}
-
-impl FailingStructSerializer {
- fn start() -> Self {
- Self {
- failure: FailingSerializeFailure::Start,
- }
- }
-
- fn field(field_index: usize) -> Self {
- Self {
- failure: FailingSerializeFailure::Field(field_index),
- }
- }
-
- fn end() -> Self {
- Self {
- failure: FailingSerializeFailure::End,
- }
- }
-}
-
-impl ser::Serializer for FailingStructSerializer {
- type Ok = ();
- type Error = FailingSerializeError;
- type SerializeSeq = ser::Impossible<(), FailingSerializeError>;
- type SerializeTuple = ser::Impossible<(), FailingSerializeError>;
- type SerializeTupleStruct = ser::Impossible<(), FailingSerializeError>;
- type SerializeTupleVariant = ser::Impossible<(), FailingSerializeError>;
- type SerializeMap = ser::Impossible<(), FailingSerializeError>;
- type SerializeStruct = FailingSerializeStruct;
- type SerializeStructVariant = ser::Impossible<(), FailingSerializeError>;
-
- fn serialize_bool(self, _value: bool) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i8(self, _value: i8) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i16(self, _value: i16) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i32(self, _value: i32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_i64(self, _value: i64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u8(self, _value: u8) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u16(self, _value: u16) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u32(self, _value: u32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_u64(self, _value: u64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_f32(self, _value: f32) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_f64(self, _value: f64) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_char(self, _value: char) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_str(self, _value: &str) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_bytes(self, _value: &[u8]) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_some<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_unit_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- ) -> Result<Self::Ok, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_newtype_struct<T>(
- self,
- _name: &'static str,
- _value: &T,
- ) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_newtype_variant<T>(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _value: &T,
- ) -> Result<Self::Ok, Self::Error>
- where
- T: ?Sized + Serialize,
- {
- Err(FailingSerializeError)
- }
-
- fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple_struct(
- self,
- _name: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeTupleStruct, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_tuple_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeTupleVariant, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
- Err(FailingSerializeError)
- }
-
- fn serialize_struct(
- self,
- _name: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeStruct, Self::Error> {
- match self.failure {
- FailingSerializeFailure::Start => Err(FailingSerializeError),
- failure => Ok(FailingSerializeStruct {
- field_index: 0,
- failure,
- }),
- }
- }
-
- fn serialize_struct_variant(
- self,
- _name: &'static str,
- _variant_index: u32,
- _variant: &'static str,
- _len: usize,
- ) -> Result<Self::SerializeStructVariant, Self::Error> {
- Err(FailingSerializeError)
- }
-}
-
-impl SerializeStruct for FailingSerializeStruct {
- type Ok = ();
- type Error = FailingSerializeError;
-
- fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error>
- where
- T: ?Sized + Serialize,
- {
- self.field_index += 1;
- match self.failure {
- FailingSerializeFailure::Field(field) if self.field_index == field => {
- Err(FailingSerializeError)
- }
- _ => Ok(()),
- }
- }
-
- fn end(self) -> Result<Self::Ok, Self::Error> {
- match self.failure {
- FailingSerializeFailure::End => Err(FailingSerializeError),
- _ => Ok(()),
- }
- }
-}
-
-pub fn assert_struct_serialize_error_paths<T>(value: &T, field_count: usize)
-where
- T: Serialize,
-{
- value
- .serialize(FailingStructSerializer::start())
- .expect_err("struct start failure");
- for field_index in 1..=field_count {
- value
- .serialize(FailingStructSerializer::field(field_index))
- .expect_err("struct field failure");
- }
- value
- .serialize(FailingStructSerializer::end())
- .expect_err("struct end failure");
-}
diff --git a/crates/sdk/tests/unit/actor_json_tests.rs b/crates/sdk/tests/unit/actor_json_tests.rs
@@ -1,76 +0,0 @@
-use super::{SdkActorContextJson, actor_role_code, actor_source_code};
-use radroots_event::contract::AuthorRole;
-use radroots_identity::AccountId;
-use radroots_signing::{Actor, actor::ActorSource};
-
-use crate::serializer_failure::assert_struct_serialize_error_paths;
-
-const PUBKEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-
-#[test]
-fn actor_role_and_source_codes_cover_public_actor_taxonomy() {
- assert_eq!(actor_role_code(&AuthorRole::Any), "any");
- assert_eq!(actor_role_code(&AuthorRole::Application), "application");
- assert_eq!(actor_role_code(&AuthorRole::Buyer), "buyer");
- assert_eq!(actor_role_code(&AuthorRole::Farmer), "farmer");
- assert_eq!(actor_role_code(&AuthorRole::Member), "member");
- assert_eq!(actor_role_code(&AuthorRole::Moderator), "moderator");
- assert_eq!(actor_role_code(&AuthorRole::Relay), "relay");
- assert_eq!(actor_role_code(&AuthorRole::Seller), "seller");
- assert_eq!(actor_role_code(&AuthorRole::Service), "service");
-
- assert_eq!(
- actor_source_code(ActorSource::LocalAccount(account_id())),
- "local_account"
- );
- assert_eq!(
- actor_source_code(ActorSource::ExplicitPublicKey),
- "explicit_public_key"
- );
- assert_eq!(
- actor_source_code(ActorSource::RemoteSigner(account_id())),
- "remote_signer"
- );
- assert_eq!(
- actor_source_code(ActorSource::Service(account_id())),
- "service"
- );
-}
-
-#[test]
-fn actor_context_json_preserves_source_roles_and_account_id() {
- let actor = Actor::from_public_key_hex(
- PUBKEY,
- ActorSource::LocalAccount(account_id()),
- [AuthorRole::Buyer, AuthorRole::Seller],
- )
- .expect("actor");
-
- let json = serde_json::to_value(SdkActorContextJson(&actor)).expect("actor json");
-
- assert_eq!(
- json,
- serde_json::json!({
- "pubkey": PUBKEY,
- "roles": ["buyer", "seller"],
- "account_id": PUBKEY,
- "source": "local_account"
- })
- );
-}
-
-#[test]
-fn actor_context_json_reports_serializer_failures() {
- let actor = Actor::from_public_key_hex(
- PUBKEY,
- ActorSource::LocalAccount(account_id()),
- [AuthorRole::Buyer, AuthorRole::Seller],
- )
- .expect("actor");
-
- assert_struct_serialize_error_paths(&SdkActorContextJson(&actor), 4);
-}
-
-fn account_id() -> AccountId {
- AccountId::from_hex(PUBKEY).expect("account ID")
-}
diff --git a/crates/sdk/tests/unit/adapters_nostr_tests.rs b/crates/sdk/tests/unit/adapters_nostr_tests.rs
@@ -1,85 +0,0 @@
-use super::{
- client_from_keys, configure_write_relays, connected_client_from_keys, connected_relay_urls,
- publish_signed_event, signerless_client, signerless_client_with_options,
-};
-use core::time::Duration;
-use nostr::{EventBuilder, Keys, Kind};
-use radroots_transport_nostr::{RadrootsNostrClientOptions, RadrootsRelayTransportError};
-use tokio::runtime::Runtime;
-
-#[test]
-fn client_constructors_build_without_runtime_net() {
- let keys = Keys::generate();
- let _client = client_from_keys(keys);
- let _signerless = signerless_client();
- let _signerless_with_options =
- signerless_client_with_options(RadrootsNostrClientOptions::new());
-}
-
-#[test]
-fn signerless_client_has_no_signer() {
- let runtime = Runtime::new().expect("tokio runtime");
- runtime.block_on(async {
- let client = signerless_client();
- assert!(!client.has_signer().await);
- });
-}
-
-#[test]
-fn relay_helpers_accept_empty_relay_sets_without_network_endpoints() {
- let runtime = Runtime::new().expect("tokio runtime");
- runtime.block_on(async {
- let keys = Keys::generate();
- let client = client_from_keys(keys.clone());
-
- configure_write_relays(&client, &[], Duration::from_millis(1))
- .await
- .expect("configure empty relays");
- assert_eq!(connected_relay_urls(&client).await, Vec::<String>::new());
-
- let invalid_relays = vec!["not-a-relay-url".to_owned()];
- let error = configure_write_relays(&client, &invalid_relays, Duration::from_millis(1))
- .await
- .expect_err("invalid relay");
- assert!(matches!(error, RadrootsRelayTransportError::Client(_)));
- let connected_error = match connected_client_from_keys(
- keys.clone(),
- &invalid_relays,
- Duration::from_millis(1),
- )
- .await
- {
- Ok(_) => panic!("expected invalid connected relay"),
- Err(error) => error,
- };
- assert!(matches!(
- connected_error,
- RadrootsRelayTransportError::Client(_)
- ));
-
- let disconnected = client_from_keys(keys.clone());
- disconnected
- .add_write_relay("wss://relay.example.com")
- .await
- .expect("add relay");
- assert_eq!(
- connected_relay_urls(&disconnected).await,
- Vec::<String>::new()
- );
-
- let connected = connected_client_from_keys(keys.clone(), &[], Duration::from_millis(1))
- .await
- .expect("connected client");
- assert_eq!(connected_relay_urls(&connected).await, Vec::<String>::new());
-
- // Relay publication consumes an already-signed transport fixture; it
- // does not expose an SDK event-authoring path.
- let signed = EventBuilder::new(Kind::Custom(30_001), "hello")
- .sign_with_keys(&keys)
- .expect("signed event");
- let error = publish_signed_event(&connected, &signed)
- .await
- .expect_err("publish without relays");
- assert!(matches!(error, RadrootsRelayTransportError::Client(_)));
- });
-}
diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs
@@ -1,573 +0,0 @@
-use super::{
- RadrootsSdkError, RadrootsSdkGeoNamesErrorKind, RadrootsSdkListingValidationErrorKind,
- RadrootsSdkTradeErrorKind, radroots_sdk_error_catalog, redacted_relay_url,
-};
-use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField};
-use crate::transport::ReticulumBehavior;
-use radroots_geocoder::{GeoNamesAssetFetcher, GeoNamesBlockingHttpFetcher, GeocoderError};
-use radroots_signing::{Error as SigningError, error::Kind as SigningErrorKind};
-use std::collections::BTreeSet;
-
-#[test]
-fn signing_error_conversion_preserves_normalized_failures() {
- let actor_error =
- RadrootsSdkError::from(SigningError::new(SigningErrorKind::AuthorizationDenied));
- assert!(matches!(
- actor_error,
- RadrootsSdkError::UnauthorizedActor { ref reason, .. }
- if reason == "actor or signer is not authorized for the frozen draft"
- ));
-
- let fallback = RadrootsSdkError::from(SigningError::new(SigningErrorKind::InvalidArgument));
- assert!(matches!(
- fallback,
- RadrootsSdkError::Authority { ref message } if message == "signing request is invalid"
- ));
-}
-
-#[test]
-fn listing_and_store_errors_convert_to_sdk_error_classes() {
- let draft_fallback = RadrootsSdkError::from(
- radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidFarmPubkey(
- radroots_identity::PublicKey::from_hex("bad").expect_err("invalid public key"),
- ),
- );
- assert!(matches!(
- draft_fallback,
- RadrootsSdkError::ListingEdit { ref message }
- if message.contains("invalid listing edit farm pubkey")
- ));
-
- let invalid_model = RadrootsSdkError::from(
- radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidModel(
- radroots_event::trade::validation::OperationalListingValidationError::MissingInventory,
- ),
- );
- assert!(matches!(
- invalid_model,
- RadrootsSdkError::ListingValidation {
- kind: RadrootsSdkListingValidationErrorKind::MissingInventory,
- ref message,
- } if message == "missing listing inventory"
- ));
- let detail = invalid_model.detail_json();
- assert_eq!(detail["code"], "listing_validation");
- assert_eq!(
- detail["detail"]["kind"],
- serde_json::json!("missing_inventory")
- );
- assert_eq!(detail["detail"]["message"], "missing listing inventory");
-
- let mutation = RadrootsSdkError::from(
- radroots_trade::operational_listing::RadrootsOperationalListingMutationError::UnsupportedMutation,
- );
- assert!(matches!(
- mutation,
- RadrootsSdkError::ListingMutation { ref message }
- if message == "listing mutation is not supported"
- ));
-
- let store = RadrootsSdkError::from(
- radroots_event_store::RadrootsEventStoreError::MissingEvent("event-a".to_owned()),
- );
- assert!(matches!(
- store,
- RadrootsSdkError::EventStore { ref message } if message.contains("event-a")
- ));
-}
-
-#[test]
-fn outbox_error_conversion_handles_empty_targets_and_fallbacks() {
- assert!(matches!(
- RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets),
- RadrootsSdkError::EmptyTransportTargets { ref operation } if operation == "outbox enqueue"
- ));
-
- assert!(matches!(
- RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::EventNotFound(42)),
- RadrootsSdkError::Outbox { ref message } if message.contains("42")
- ));
- assert!(matches!(
- RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::IdempotencyConflict {
- operation_kind: "listing.publish.v1".to_owned(),
- expected_pubkey: "a".repeat(64),
- idempotency_key: "idem-1".to_owned(),
- existing_digest: "b".repeat(64),
- new_digest: "c".repeat(64),
- }),
- RadrootsSdkError::IdempotencyConflict {
- ref operation_kind,
- ref expected_pubkey_prefix,
- ref existing_digest_prefix,
- ref new_digest_prefix,
- } if operation_kind == "listing.publish.v1"
- && expected_pubkey_prefix == "aaaaaaaaaaaa"
- && existing_digest_prefix == "bbbbbbbbbbbb"
- && new_digest_prefix == "cccccccccccc"
- ));
-}
-
-#[test]
-fn relay_transport_error_conversion_redacts_and_classifies_url_errors() {
- let unsupported = RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::UnsupportedRelayScheme {
- url: "ftp://user:secret@relay.example.com/path?token=secret".to_owned(),
- scheme: "ftp".to_owned(),
- },
- );
- assert!(matches!(
- unsupported,
- RadrootsSdkError::InvalidRelayUrl { ref url, ref reason }
- if url == "ftp://<redacted>@relay.example.com/path?<redacted>"
- && reason == "unsupported scheme `ftp`"
- ));
-
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::EmptyRelayHost {
- url: "wss://".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref reason, .. }
- if reason == "relay URL must include a host"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlQueryOrFragment {
- url: "wss://relay.example.com?token=secret".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref url, .. }
- if url == "wss://relay.example.com?<redacted>"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlUserinfo {
- url: "wss://user:secret@relay.example.com".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref url, ref reason }
- if url == "wss://<redacted>@relay.example.com"
- && reason == "relay URL must not include userinfo"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::WsRequiresLocalhostPolicy {
- url: "ws://relay.example.com".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref reason, .. }
- if reason == "ws relay URL requires localhost policy"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlForbiddenDestination {
- url: "ws://127.0.0.1:9000".to_owned(),
- reason: "localhost disabled".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref reason, .. }
- if reason == "localhost disabled"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination {
- url: "ws://relay.example.com".to_owned(),
- address: "127.0.0.1".to_owned(),
- reason: "loopback disabled".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref reason, .. }
- if reason == "relay URL resolved to forbidden address `127.0.0.1`: loopback disabled"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::EmptyTargetSet
- ),
- RadrootsSdkError::EmptyTransportTargets { ref operation } if operation == "nostr relay publish"
- ));
- assert!(matches!(
- RadrootsSdkError::from(
- radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlParse {
- url: "wss://user:secret@relay.example.com/path?token=secret".to_owned(),
- reason: "bad relay URL".to_owned(),
- },
- ),
- RadrootsSdkError::InvalidRelayUrl { ref url, ref reason }
- if url == "wss://<redacted>@relay.example.com/path?<redacted>"
- && reason == "bad relay URL"
- ));
- assert!(matches!(
- RadrootsSdkError::from(radroots_transport_nostr::RadrootsRelayTransportError::Outbox(
- radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets,
- )),
- RadrootsSdkError::EmptyTransportTargets { ref operation } if operation == "outbox enqueue"
- ));
- assert!(matches!(
- RadrootsSdkError::from(radroots_transport_nostr::RadrootsRelayTransportError::Transport(
- "offline".to_owned(),
- )),
- RadrootsSdkError::Transport { ref message } if message == "Relay transport error: offline"
- ));
-}
-
-#[test]
-fn relay_url_redaction_handles_plain_values_and_userinfo() {
- assert_eq!(redacted_relay_url("not-a-url".to_owned()), "not-a-url");
- assert_eq!(
- redacted_relay_url("not-a-url?token=secret".to_owned()),
- "not-a-url?<redacted>"
- );
- assert_eq!(
- redacted_relay_url("not-a-url#fragment".to_owned()),
- "not-a-url#<redacted>"
- );
- assert_eq!(
- redacted_relay_url("wss://relay.example.com/path?token=secret".to_owned()),
- "wss://relay.example.com/path?<redacted>"
- );
- assert_eq!(
- redacted_relay_url("wss://user:secret@relay.example.com/path#frag".to_owned()),
- "wss://<redacted>@relay.example.com/path#<redacted>"
- );
- assert_eq!(
- redacted_relay_url("wss://relay.example.com/path#fragment".to_owned()),
- "wss://relay.example.com/path#<redacted>"
- );
- assert_eq!(
- redacted_relay_url("wss://relay.example.com/path".to_owned()),
- "wss://relay.example.com/path"
- );
-}
-
-#[test]
-fn sdk_error_contract_methods_cover_representative_classes_and_details() {
- let errors = vec![
- RadrootsSdkError::Io {
- path: "store.sqlite".into(),
- message: "readonly".to_owned(),
- },
- RadrootsSdkError::ClockBeforeUnixEpoch,
- RadrootsSdkError::TimestampOutOfRange { value: u64::MAX },
- RadrootsSdkError::UnauthorizedActor {
- operation: "listing.publish".to_owned(),
- reason: "missing seller".to_owned(),
- },
- RadrootsSdkError::SignerPubkeyMismatch {
- operation: "listing.publish".to_owned(),
- expected_pubkey_prefix: "aaaaaaaaaaaa".to_owned(),
- signer_pubkey_prefix: "bbbbbbbbbbbb".to_owned(),
- },
- RadrootsSdkError::SignerUnavailable {
- mode: "configured".to_owned(),
- reason: "missing".to_owned(),
- },
- RadrootsSdkError::SignerRequestRejected {
- mode: "myc_nip46".to_owned(),
- reason: "denied".to_owned(),
- },
- RadrootsSdkError::SignerRequestTimedOut {
- mode: "myc_nip46".to_owned(),
- },
- RadrootsSdkError::SignerAuthChallengePending {
- mode: "myc_nip46".to_owned(),
- auth_url: Some("https://auth.example.com/challenge".to_owned()),
- },
- RadrootsSdkError::SignerAuthChallengePending {
- mode: "myc_nip46".to_owned(),
- auth_url: None,
- },
- RadrootsSdkError::SignerTransport {
- mode: "myc_nip46".to_owned(),
- reason: "offline".to_owned(),
- },
- RadrootsSdkError::SignerProtocol {
- mode: "myc_nip46".to_owned(),
- reason: "bad envelope".to_owned(),
- },
- RadrootsSdkError::SignerReturnedEventDrift {
- operation: "listing.publish".to_owned(),
- reason: "id changed".to_owned(),
- },
- RadrootsSdkError::EmptyTransportTargets {
- operation: "nostr relay publish".to_owned(),
- },
- RadrootsSdkError::TransportTargetLimitExceeded { max: 2, actual: 3 },
- RadrootsSdkError::invalid_relay_url(
- "wss://user:secret@relay.example.com/path?token=secret",
- "userinfo",
- ),
- RadrootsSdkError::IdempotencyConflict {
- operation_kind: "listing.publish.v1".to_owned(),
- expected_pubkey_prefix: "aaaaaaaaaaaa".to_owned(),
- existing_digest_prefix: "existing".to_owned(),
- new_digest_prefix: "new".to_owned(),
- },
- RadrootsSdkError::Trade {
- kind: RadrootsSdkTradeErrorKind::QueryLimitInvalid,
- operation: "trade.list".to_owned(),
- message: "limit out of range".to_owned(),
- },
- RadrootsSdkError::Trade {
- kind: RadrootsSdkTradeErrorKind::PrivateArtifactMissing,
- operation: "trade.submit_proposal".to_owned(),
- message: "private artifact missing".to_owned(),
- },
- RadrootsSdkError::PrivacyPreflight {
- operation: "trade.cancel".to_owned(),
- status: PrivacyPreflightStatus::ExplicitConfirmationRequired,
- fields: vec![ProductSensitivityField::PublicButSensitiveNotes],
- },
- RadrootsSdkError::ProductSyncUnsupported {
- operation: "sync.push_outbox",
- required_feature: "transport-nostr-runtime",
- },
- RadrootsSdkError::ReticulumTransportUnavailable {
- operation: "sync.push_outbox".to_owned(),
- endpoint_uri: "reticulum:local".to_owned(),
- behavior: ReticulumBehavior::RejectDeliveryAttempts,
- },
- RadrootsSdkError::ReticulumTransportUnavailable {
- operation: "sync.push_outbox".to_owned(),
- endpoint_uri: "reticulum:local".to_owned(),
- behavior: ReticulumBehavior::DeferDeliveryPlans,
- },
- RadrootsSdkError::ProductSyncTransportSetupFailure {
- message: "offline".to_owned(),
- },
- RadrootsSdkError::Authority {
- message: "authority".to_owned(),
- },
- RadrootsSdkError::EventStore {
- message: "event store".to_owned(),
- },
- RadrootsSdkError::InvalidRequest {
- message: "invalid".to_owned(),
- },
- RadrootsSdkError::ListingEdit {
- message: "edit".to_owned(),
- },
- RadrootsSdkError::ListingValidation {
- kind: RadrootsSdkListingValidationErrorKind::MissingInventory,
- message: "missing listing inventory".to_owned(),
- },
- RadrootsSdkError::ListingMutation {
- message: "mutation".to_owned(),
- },
- RadrootsSdkError::Outbox {
- message: "outbox".to_owned(),
- },
- RadrootsSdkError::PrivateStore {
- message: "private".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Configuration,
- message: "missing cache root".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Download,
- message: "download".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Cache,
- message: "cache".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Integrity,
- message: "integrity".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Schema,
- message: "schema".to_owned(),
- },
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Lookup,
- message: "lookup".to_owned(),
- },
- RadrootsSdkError::Transport {
- message: "transport".to_owned(),
- },
- RadrootsSdkError::Projection {
- message: "projection".to_owned(),
- },
- ];
-
- for error in errors {
- let detail = error.detail_json();
- assert_eq!(detail["code"], error.code());
- assert_eq!(
- detail["class"],
- serde_json::to_value(error.class()).expect("class json")
- );
- assert_eq!(detail["retryable"], error.retryable());
- assert_eq!(
- detail["recovery_actions"],
- serde_json::to_value(error.recovery_actions()).expect("recovery json")
- );
- assert!(error.to_string().starts_with("sdk "));
- }
-}
-
-#[test]
-fn sdk_error_catalog_exposes_stable_codes_and_metadata() {
- let catalog = radroots_sdk_error_catalog();
- let codes = catalog
- .iter()
- .map(|entry| entry.code)
- .collect::<BTreeSet<_>>();
-
- assert_eq!(codes.len(), catalog.len());
- assert!(codes.contains("io"));
- assert!(codes.contains("signer_auth_challenge_pending"));
- assert!(codes.contains("product_sync_unsupported"));
- assert!(codes.contains("reticulum_transport_deferred"));
- assert!(codes.contains("unsupported_profile_schema"));
- assert!(codes.contains("listing_validation"));
- assert!(codes.contains("geonames_lookup"));
-
- for trade_kind in [
- RadrootsSdkTradeErrorKind::InvalidEnvelope,
- RadrootsSdkTradeErrorKind::InvalidCommandBody,
- RadrootsSdkTradeErrorKind::PrivateArtifactMissing,
- RadrootsSdkTradeErrorKind::PrivateArtifactCommitmentMismatch,
- RadrootsSdkTradeErrorKind::PrivateArtifactAcknowledgementMissing,
- RadrootsSdkTradeErrorKind::TradeNotFound,
- RadrootsSdkTradeErrorKind::QueryLimitInvalid,
- RadrootsSdkTradeErrorKind::CursorInvalid,
- ] {
- assert!(codes.contains(trade_kind.code()));
- }
-
- for entry in catalog {
- assert!(!entry.code.is_empty());
- assert!(!entry.recovery_actions.is_empty());
- }
-
- let timeout = RadrootsSdkError::SignerRequestTimedOut {
- mode: "myc_nip46".to_owned(),
- };
- let timeout_entry = catalog
- .iter()
- .find(|entry| entry.code == timeout.code())
- .expect("timeout catalog entry");
- assert_eq!(timeout_entry.class, timeout.class());
- assert_eq!(timeout_entry.retryable, timeout.retryable());
- let timeout_recovery_actions = timeout.recovery_actions();
- assert_eq!(
- timeout_entry.recovery_actions,
- timeout_recovery_actions.as_slice()
- );
-
- let trade = RadrootsSdkError::Trade {
- kind: RadrootsSdkTradeErrorKind::PrivateArtifactCommitmentMismatch,
- operation: "trade.decide_candidate".to_owned(),
- message: "private artifact commitment mismatch".to_owned(),
- };
- let trade_entry = catalog
- .iter()
- .find(|entry| entry.code == trade.code())
- .expect("trade catalog entry");
- assert_eq!(trade_entry.class, trade.class());
- assert_eq!(trade_entry.retryable, trade.retryable());
- let trade_recovery_actions = trade.recovery_actions();
- assert_eq!(
- trade_entry.recovery_actions,
- trade_recovery_actions.as_slice()
- );
-}
-
-#[test]
-fn geonames_error_conversion_maps_source_errors_to_sdk_kinds() {
- let path = std::path::PathBuf::from("geonames-test.db");
- let download_error = GeoNamesBlockingHttpFetcher
- .fetch("not-a-url")
- .expect_err("invalid URL download error");
- assert!(matches!(
- RadrootsSdkError::from(download_error),
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Download,
- ..
- }
- ));
-
- let cases = vec![
- (
- GeocoderError::InvalidAssetUrl {
- url: "http://assets.radroots.io/geonames-1.0.db".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Configuration,
- ),
- (
- GeocoderError::InvalidAssetHost {
- url: "https://example.com/geonames-1.0.db".to_owned(),
- expected_host: "assets.radroots.io".to_owned(),
- actual_host: "example.com".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Configuration,
- ),
- (
- GeocoderError::InvalidAssetLength {
- path: path.clone(),
- expected: 4,
- actual: 3,
- },
- RadrootsSdkGeoNamesErrorKind::Integrity,
- ),
- (
- GeocoderError::InvalidAssetSha256 {
- path: path.clone(),
- expected: "a".repeat(64),
- actual: "b".repeat(64),
- },
- RadrootsSdkGeoNamesErrorKind::Integrity,
- ),
- (
- GeocoderError::InvalidAssetSqlite {
- path: path.clone(),
- detail: "file is not a database".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Integrity,
- ),
- (
- GeocoderError::InvalidAssetIntegrity {
- path: path.clone(),
- result: "row mismatch".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Integrity,
- ),
- (
- GeocoderError::InvalidAssetSchema {
- path: path.clone(),
- detail: "missing table".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Schema,
- ),
- (
- GeocoderError::AssetLockUnavailable { path: path.clone() },
- RadrootsSdkGeoNamesErrorKind::Cache,
- ),
- (
- GeocoderError::SqliteConnectionLockUnavailable,
- RadrootsSdkGeoNamesErrorKind::Cache,
- ),
- (
- GeocoderError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "missing")),
- RadrootsSdkGeoNamesErrorKind::Cache,
- ),
- (
- GeocoderError::CountryCenterNotFound {
- country_id: "XX".to_owned(),
- },
- RadrootsSdkGeoNamesErrorKind::Lookup,
- ),
- ];
-
- for (source, expected_kind) in cases {
- let error = RadrootsSdkError::from(source);
-
- assert!(matches!(
- error,
- RadrootsSdkError::GeoNames { kind, .. } if kind == expected_kind
- ));
- }
-}
diff --git a/crates/sdk/tests/unit/idempotency_tests.rs b/crates/sdk/tests/unit/idempotency_tests.rs
@@ -1,90 +0,0 @@
-use super::{SdkIdempotencyKey, SdkTradeIdempotencyRecord};
-use crate::RadrootsSdkError;
-use radroots_event::id::EventId;
-use radroots_identity::PublicKey;
-
-use crate::serializer_failure::assert_struct_serialize_error_paths;
-
-#[test]
-fn empty_key_is_rejected_before_redacted_storage() {
- assert!(matches!(
- SdkIdempotencyKey::new(""),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "idempotency key must not be empty"
- ));
- assert!(matches!(
- SdkIdempotencyKey::new(" key"),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "idempotency key must not include boundary whitespace"
- ));
- assert!(matches!(
- SdkIdempotencyKey::new("key\nvalue"),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "idempotency key must not contain control characters"
- ));
- assert!(matches!(
- SdkIdempotencyKey::new("k".repeat(super::SDK_IDEMPOTENCY_KEY_MAX_LEN + 1)),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message.contains("idempotency key must be at most")
- ));
-}
-
-#[test]
-fn explicit_uuid_v7_key_is_accepted_and_other_shapes_are_rejected() {
- let key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000001").expect("key");
-
- assert_eq!(key.as_str(), "01890f0e-6c00-7000-8000-000000000001");
- assert_eq!(key.into_string(), "01890f0e-6c00-7000-8000-000000000001");
- assert!(matches!(
- SdkIdempotencyKey::new("01890f0e-6c00-6000-8000-000000000001"),
- Err(RadrootsSdkError::InvalidRequest { ref message })
- if message == "idempotency key must be a UUIDv7"
- ));
-}
-
-#[test]
-fn idempotency_key_reports_serializer_failures() {
- let key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000002").expect("key");
-
- assert_struct_serialize_error_paths(&key, 2);
-}
-
-#[test]
-fn trade_idempotency_record_binds_payload_and_reports_conflicts() {
- let record = SdkTradeIdempotencyRecord {
- idempotency_key: SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000003")
- .expect("key"),
- operation_kind: "trade.submit.v1".to_owned(),
- actor_pubkey: PublicKey::from_hex(
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- )
- .expect("actor pubkey"),
- digest: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_owned(),
- canonical_payload_hash: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
- .to_owned(),
- expected_event_id: EventId::parse(
- "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
- )
- .expect("event id"),
- outbox_operation_id: 42,
- };
-
- assert!(
- record.matches_payload("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
- );
- assert!(
- !record.matches_payload("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
- );
- assert!(matches!(
- record.conflict_error("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),
- RadrootsSdkError::IdempotencyConflict {
- ref operation_kind,
- ref expected_pubkey_prefix,
- ref existing_digest_prefix,
- ref new_digest_prefix,
- } if operation_kind == "trade.submit.v1"
- && expected_pubkey_prefix == "aaaaaaaaaaaa"
- && existing_digest_prefix == "bbbbbbbbbbbb"
- && new_digest_prefix == "ffffffffffff"
- ));
-}
diff --git a/crates/sdk/tests/unit/private_store_tests.rs b/crates/sdk/tests/unit/private_store_tests.rs
@@ -1,290 +0,0 @@
-use super::*;
-use radroots_event::id::AddressableCoordinate;
-use sqlx::Row;
-use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
-
-fn farm_addr_for(d_tag: &str) -> AddressableCoordinate {
- AddressableCoordinate::parse(format!(
- "{}:{}:{}",
- radroots_event::envelope::kind::KIND_FARM,
- "a".repeat(64),
- d_tag
- ))
- .expect("farm addr")
-}
-
-fn farm_addr() -> AddressableCoordinate {
- farm_addr_for("AAAAAAAAAAAAAAAAAAAAAA")
-}
-
-fn private_location_record() -> SdkPrivateFarmLocationRecord {
- SdkPrivateFarmLocationRecord {
- farm_addr: farm_addr(),
- farm_pubkey: "a".repeat(64),
- farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
- label: Some("Main pickup point".to_owned()),
- latitude: 12.26,
- longitude: -34.51,
- locality_primary: "Fixture Town".to_owned(),
- locality_city: Some("Fixture Town".to_owned()),
- locality_region: Some("Fixture Region".to_owned()),
- locality_country: Some("Fixture Country".to_owned()),
- geohash5: "e4pmw".to_owned(),
- geonames_feature_id: Some(1),
- geonames_country_id: Some("FX".to_owned()),
- updated_at_ms: 1_700_000_123_000,
- }
-}
-
-#[tokio::test]
-async fn private_store_file_open_rejects_directory_paths() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- assert!(matches!(
- SdkPrivateStore::open_file(tempdir.path()).await,
- Err(RadrootsSdkError::PrivateStore { .. })
- ));
-}
-
-#[tokio::test]
-async fn private_store_status_update_delete_and_pragmas_round_trip() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let path = tempdir.path().join("private.sqlite");
- let store = SdkPrivateStore::open_file(&path).await.expect("open store");
- assert_eq!(store.pragma_foreign_keys().await.expect("foreign keys"), 1);
- assert_eq!(
- store.pragma_busy_timeout().await.expect("busy timeout"),
- 5_000
- );
- assert_eq!(
- store.pragma_journal_mode().await.expect("journal mode"),
- "wal"
- );
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("empty status")
- .farm_private_locations,
- 0
- );
-
- let record = private_location_record();
- assert_eq!(
- store
- .farm_location(&record.farm_addr)
- .await
- .expect("missing lookup"),
- None
- );
- store
- .upsert_farm_location(&record)
- .await
- .expect("insert private location");
- assert_private_farm_location_payload_is_encrypted(&store, &record).await;
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("inserted status")
- .farm_private_locations,
- 1
- );
- assert_eq!(
- store
- .farm_location(&record.farm_addr)
- .await
- .expect("stored lookup"),
- Some(record.clone())
- );
-
- let mut updated = record.clone();
- updated.label = None;
- updated.latitude = 12.5;
- updated.longitude = -34.75;
- updated.locality_primary = "Updated Town".to_owned();
- updated.locality_city = Some("Updated Town".to_owned());
- updated.geonames_feature_id = Some(2);
- updated.updated_at_ms = 1_700_000_124_000;
- store
- .upsert_farm_location(&updated)
- .await
- .expect("update private location");
- assert_eq!(
- store
- .farm_location(&record.farm_addr)
- .await
- .expect("updated lookup"),
- Some(updated.clone())
- );
-
- let missing_addr = farm_addr_for("AAAAAAAAAAAAAAAAAAAAAQ");
- assert!(
- !store
- .delete_farm_location(&missing_addr)
- .await
- .expect("delete missing")
- );
- assert!(
- store
- .delete_farm_location(&updated.farm_addr)
- .await
- .expect("delete stored")
- );
- assert!(
- !store
- .delete_farm_location(&updated.farm_addr)
- .await
- .expect("delete already cleared")
- );
- assert_eq!(
- store
- .status_summary()
- .await
- .expect("cleared status")
- .farm_private_locations,
- 0
- );
-}
-
-#[tokio::test]
-async fn private_store_file_open_rejects_pre_v1_schema_without_repair() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let path = tempdir.path().join("private.sqlite");
- let options = SqliteConnectOptions::new()
- .filename(&path)
- .create_if_missing(true);
- let pool = SqlitePoolOptions::new()
- .max_connections(1)
- .connect_with(options)
- .await
- .expect("old private store pool");
- sqlx::raw_sql(
- r#"
- CREATE TABLE sdk_private_farm_location (
- farm_addr TEXT PRIMARY KEY NOT NULL,
- farm_pubkey TEXT NOT NULL,
- farm_d_tag TEXT NOT NULL,
- latitude REAL NOT NULL CHECK(latitude >= -90.0 AND latitude <= 90.0),
- longitude REAL NOT NULL CHECK(longitude >= -180.0 AND longitude <= 180.0),
- locality_primary TEXT NOT NULL,
- locality_city TEXT,
- locality_region TEXT,
- locality_country TEXT,
- geohash5 TEXT NOT NULL CHECK(length(geohash5) = 5),
- geonames_feature_id INTEGER,
- geonames_country_id TEXT,
- updated_at_ms INTEGER NOT NULL
- );
- "#,
- )
- .execute(&pool)
- .await
- .expect("old private store schema");
- pool.close().await;
-
- assert!(matches!(
- SdkPrivateStore::open_file(&path).await,
- Err(RadrootsSdkError::UnsupportedProfileSchema { .. })
- ));
-}
-
-#[tokio::test]
-async fn private_store_schema_uses_v1_private_authority_tables() {
- let store = SdkPrivateStore::open_memory().await.expect("private store");
- let tables = sqlx::query(
- r#"
- SELECT name FROM sqlite_master
- WHERE type = 'table'
- ORDER BY name
- "#,
- )
- .fetch_all(store.pool())
- .await
- .expect("tables")
- .into_iter()
- .map(|row| row.try_get::<String, _>("name").expect("name"))
- .collect::<Vec<_>>();
-
- for table in [
- "cursor_hmac_key",
- "key_rotation_progress",
- "nip46_session_private",
- "private_farm_location",
- "private_metadata",
- "private_trade_artifacts",
- "wrapped_profile_key",
- "wrapped_signing_secret",
- ] {
- assert!(tables.iter().any(|name| name == table), "missing {table}");
- }
- assert!(
- !tables
- .iter()
- .any(|name| name == "sdk_private_farm_location")
- );
-
- let columns = sqlx::query("PRAGMA table_info(private_farm_location)")
- .fetch_all(store.pool())
- .await
- .expect("columns")
- .into_iter()
- .map(|row| row.try_get::<String, _>("name").expect("column name"))
- .collect::<Vec<_>>();
- for forbidden in [
- "label",
- "latitude",
- "longitude",
- "locality_primary",
- "locality_city",
- "locality_region",
- "locality_country",
- "geohash5",
- "geonames_feature_id",
- "geonames_country_id",
- ] {
- assert!(
- !columns.iter().any(|column| column == forbidden),
- "private_farm_location must not retain plaintext column {forbidden}"
- );
- }
-}
-
-async fn assert_private_farm_location_payload_is_encrypted(
- store: &SdkPrivateStore,
- record: &SdkPrivateFarmLocationRecord,
-) {
- let row = sqlx::query(
- r#"
- SELECT ciphertext, nonce
- FROM private_farm_location
- WHERE farm_kind = 30340
- "#,
- )
- .fetch_one(store.pool())
- .await
- .expect("encrypted private location row");
- let ciphertext: Vec<u8> = row.try_get("ciphertext").expect("ciphertext");
- let nonce: Vec<u8> = row.try_get("nonce").expect("nonce");
- assert_eq!(nonce.len(), 24);
- let rendered = String::from_utf8_lossy(ciphertext.as_slice());
- let forbidden_values = vec![
- record.label.clone().expect("label"),
- record.latitude.to_string(),
- record.longitude.to_string(),
- record.locality_primary.clone(),
- record.locality_city.clone().expect("city"),
- record.locality_region.clone().expect("region"),
- record.locality_country.clone().expect("country"),
- record.geohash5.clone(),
- record
- .geonames_country_id
- .clone()
- .expect("geonames country"),
- ];
- for forbidden in forbidden_values {
- assert!(
- !rendered.contains(forbidden.as_str()),
- "encrypted private location payload leaked {forbidden}"
- );
- }
-}
diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs
@@ -1,928 +0,0 @@
-use super::*;
-#[cfg(feature = "signer-adapters")]
-use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
-use nostr::Keys as RadrootsNostrKeys;
-use radroots_event::contract::AuthorRole;
-use radroots_event::draft::{EventDraft, SignedEvent, SignedEventParts};
-use radroots_event::envelope::kind::{KIND_FARM, KIND_GEOCHAT};
-use radroots_nostr::signing::sign_frozen_draft;
-use radroots_signing::{
- Error as SigningError, SignReceipt, SignRequest, SignerStatus, actor::ActorSource,
- error::Kind as SigningErrorKind, signer::BoxFuture,
-};
-use std::sync::LazyLock;
-
-struct WorkflowKeyMaterial {
- keys: RadrootsNostrKeys,
- pubkey: String,
-}
-
-static WORKFLOW_KEY_MATERIAL: LazyLock<WorkflowKeyMaterial> = LazyLock::new(|| {
- let keys = RadrootsNostrKeys::generate();
- let pubkey = keys.public_key().to_hex();
- WorkflowKeyMaterial { keys, pubkey }
-});
-
-fn farmer_pubkey() -> &'static str {
- WORKFLOW_KEY_MATERIAL.pubkey.as_str()
-}
-
-fn workflow_idempotency_key(index: u16) -> SdkIdempotencyKey {
- SdkIdempotencyKey::new(format!("01890f0e-6c00-7000-8000-00000000{index:04x}"))
- .expect("workflow idempotency")
-}
-
-struct WorkflowSigner {
- keys: RadrootsNostrKeys,
-}
-
-impl WorkflowSigner {
- fn new() -> Self {
- Self {
- keys: WORKFLOW_KEY_MATERIAL.keys.clone(),
- }
- }
-}
-
-struct FailIfCalledSigner;
-
-impl FailIfCalledSigner {
- fn new() -> Self {
- Self
- }
-}
-
-impl Signer for FailIfCalledSigner {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> {
- Box::pin(async { Ok(SignerStatus::unavailable()) })
- }
-
- fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> {
- panic!("ephemeral workflow preflight must not invoke the signer")
- }
-}
-
-struct InvalidSignatureSigner(WorkflowSigner);
-
-impl InvalidSignatureSigner {
- fn new() -> Self {
- Self(WorkflowSigner::new())
- }
-}
-
-impl Signer for InvalidSignatureSigner {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> {
- Box::pin(async { Ok(SignerStatus::unavailable()) })
- }
-
- fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> {
- Box::pin(async move {
- let receipt = self.0.sign(request.clone()).await?;
- let mut wire = receipt.signed_event().wire().clone();
- wire.sig = "0".repeat(128);
- let raw_json =
- serde_json::to_string(&wire).expect("invalid-signature fixture must serialize");
- let signed_event =
- SignedEvent::from_wire_verified_id(wire, raw_json).map_err(|source| {
- SigningError::with_source(SigningErrorKind::InternalError, source)
- })?;
- SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001)
- })
- }
-}
-
-impl Signer for WorkflowSigner {
- fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> {
- Box::pin(async { Ok(SignerStatus::unavailable()) })
- }
-
- fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> {
- Box::pin(async move {
- let signed_event =
- sign_frozen_draft(&self.keys, request.draft()).map_err(|source| {
- SigningError::with_source(SigningErrorKind::InternalError, source)
- })?;
- SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001)
- })
- }
-}
-
-fn frozen_draft_for(pubkey: &str) -> EventDraft {
- frozen_draft_for_d_tag(pubkey, "test")
-}
-
-fn frozen_draft_for_d_tag(pubkey: &str, d_tag: &str) -> EventDraft {
- EventDraft::new(
- "radroots.farm.profile.v1",
- KIND_FARM,
- 1_700_000_000,
- vec![vec!["d".to_owned(), d_tag.to_owned()]],
- "{}",
- pubkey,
- )
- .expect("frozen draft")
-}
-
-fn frozen_draft() -> EventDraft {
- frozen_draft_for("a".repeat(64).as_str())
-}
-
-fn ephemeral_draft_for(pubkey: &str) -> EventDraft {
- EventDraft::new(
- "radroots.social.geochat.v1",
- KIND_GEOCHAT,
- 1_700_000_000,
- Vec::new(),
- "Transient local message",
- pubkey,
- )
- .expect("ephemeral draft")
-}
-
-fn signed_event() -> SignedEvent {
- let draft = frozen_draft();
- let sig = "c".repeat(128);
- let raw_json = serde_json::json!({
- "id": draft.expected_event_id_hex(),
- "pubkey": draft.expected_pubkey().to_hex(),
- "created_at": draft.created_at_u64(),
- "kind": draft.kind_u32(),
- "tags": draft.tags_as_vec(),
- "content": draft.content(),
- "sig": sig,
- })
- .to_string();
- SignedEvent::new(SignedEventParts {
- id: draft.expected_event_id_hex().to_owned(),
- pubkey: draft.expected_pubkey().to_hex().to_owned(),
- created_at: draft.created_at_u64(),
- kind: draft.kind_u32(),
- tags: draft.tags_as_vec(),
- content: draft.content().to_owned(),
- sig,
- raw_json,
- })
- .expect("signed event")
-}
-
-fn nostr_profile(relay: &'static str) -> crate::TransportProfile {
- crate::TransportProfile::nostr(
- crate::NostrProfile::new([relay], crate::NostrRelayUrlPolicy::Public)
- .expect("Nostr profile"),
- )
-}
-
-fn workflow_delivery_plan() -> radroots_outbox::RadrootsOutboxDeliveryPlanInput {
- let target_set = TargetSet::nostr_relays(
- ["wss://relay.example.com"],
- crate::NostrRelayUrlPolicy::Public,
- )
- .expect("target set");
- radroots_outbox::RadrootsOutboxDeliveryPlanInput::new(
- "explicit",
- 1,
- radroots_transport::RadrootsTransportSatisfactionPolicy::all_accepted(),
- target_set.into_targets(),
- )
-}
-
-#[test]
-fn workflow_digest_and_event_helpers_cover_error_and_input_paths() {
- assert_eq!(digest_prefix("abcdef1234567890"), "abcdef123456");
- assert_eq!(
- parse_event_id("b".repeat(64).as_str(), "event id").expect("event id"),
- EventId::parse("b".repeat(64)).expect("event id")
- );
- assert!(matches!(
- parse_event_id("not-an-event-id", "signed event id"),
- Err(RadrootsSdkError::InvalidRequest { message })
- if message.contains("signed event id is invalid")
- ));
-
- let draft = frozen_draft();
-
- let signed = signed_event();
- let event = signed.envelope();
- assert_eq!(event.id(), signed.id());
- assert_eq!(event.author(), signed.pubkey());
-
- let idempotency_key =
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency");
- let input = signed_outbox_input(
- "farm.publish.v1",
- &draft,
- signed_event(),
- workflow_delivery_plan(),
- idempotency_key,
- true,
- 1_700_000_000_000,
- );
- assert_eq!(input.operation_kind, "farm.publish.v1");
- assert_eq!(
- input.delivery_plan.targets[0].uri().as_str(),
- "wss://relay.example.com"
- );
- assert!(input.event_store_inserted);
- assert_eq!(
- durable_event_persistence(
- &draft.expected_event_id_hex(),
- &RadrootsEventPersistence::Inserted { seq: 7 },
- )
- .expect("inserted persistence"),
- (true, 7)
- );
- assert_eq!(
- durable_event_persistence(
- &draft.expected_event_id_hex(),
- &RadrootsEventPersistence::Duplicate { seq: 7 },
- )
- .expect("duplicate persistence"),
- (false, 7)
- );
- assert!(matches!(
- durable_event_persistence(
- &draft.expected_event_id_hex(),
- &RadrootsEventPersistence::NotPersisted,
- ),
- Err(RadrootsSdkError::InvalidRequest { message })
- if message.contains("requires durable local event-store persistence")
- ));
- let frozen = frozen_draft_json(&draft).expect("frozen draft json");
- assert!(frozen.contains("\"expected_event_id\""));
- let receipt = SdkWorkflowEnqueueReceipt {
- signed_event_id: EventId::parse(draft.expected_event_id_hex()).expect("event id"),
- local_event_seq: 1,
- outbox_operation_id: 2,
- outbox_event_id: 3,
- state: radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted,
- idempotency_digest_prefix: "abcdef123456".to_owned(),
- };
- let receipt_json = workflow_receipt_result_json(&receipt);
- let decoded =
- workflow_receipt_from_result_json(receipt_json.to_string().as_str()).expect("receipt");
- assert_eq!(decoded.outbox_event_id, receipt.outbox_event_id);
- assert_eq!(outbox_enqueue_status_str(decoded.state), "inserted");
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_rejects_ephemeral_event_before_durable_commit() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_013,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = ephemeral_draft_for(farmer_pubkey());
- let error = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "workflow.ephemeral.test.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::LocalOnly,
- satisfaction_policy: SatisfactionPolicy::NoWait,
- idempotency_key: Some(workflow_idempotency_key(0x247)),
- },
- &FailIfCalledSigner::new(),
- )
- .await
- .expect_err("ephemeral workflow");
-
- assert!(matches!(
- error,
- RadrootsSdkError::InvalidRequest { ref message }
- if message.contains("cannot enqueue ephemeral event kind")
- ));
- assert!(!error.retryable());
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- assert_eq!(
- sdk._outbox
- .status_summary(0)
- .await
- .expect("outbox status")
- .total_events,
- 0
- );
- let journal_count: i64 = sqlx::query_scalar(
- "SELECT COUNT(*) FROM sdk_runtime_operation_journal WHERE operation_kind = ?",
- )
- .bind("workflow.ephemeral.test.v1")
- .fetch_one(sdk._event_store.pool())
- .await
- .expect("journal count");
- assert_eq!(journal_count, 0);
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_rejects_invalid_signer_signature_without_storage_mutation() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_013,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-invalid-signature");
- let operation_kind = "farm.publish.v1";
-
- let error = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind,
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::LocalOnly,
- satisfaction_policy: SatisfactionPolicy::NoWait,
- idempotency_key: Some(workflow_idempotency_key(0x248)),
- },
- &InvalidSignatureSigner::new(),
- )
- .await
- .expect_err("invalid signer signature");
-
- assert!(matches!(
- error,
- RadrootsSdkError::SignerReturnedEventDrift {
- ref operation,
- ref reason,
- } if operation == operation_kind
- && reason.contains("failed NIP-01 verification")
- ));
- assert!(!error.retryable());
- assert_eq!(
- error.recovery_actions(),
- vec![crate::RadrootsSdkRecoveryAction::ConfigureSigner]
- );
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- assert_eq!(
- sdk._outbox
- .status_summary(0)
- .await
- .expect("outbox status")
- .total_events,
- 0
- );
- let journal_state: String = sqlx::query_scalar(
- "SELECT state FROM sdk_runtime_operation_journal WHERE operation_kind = ?",
- )
- .bind(operation_kind)
- .fetch_one(sdk._event_store.pool())
- .await
- .expect("journal state");
- assert_eq!(journal_state, "rejected");
-}
-
-#[tokio::test]
-async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_command_hash() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_012,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let signer = WorkflowSigner::new();
- let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-target-policy");
- let first_target_policy = TargetPolicy::try_nostr_relays(
- ["wss://relay-a.example.com"],
- crate::NostrRelayUrlPolicy::Public,
- )
- .expect("first target policy");
- let second_target_policy = TargetPolicy::try_nostr_relays(
- ["wss://relay-b.example.com"],
- crate::NostrRelayUrlPolicy::Public,
- )
- .expect("second target policy");
-
- let first = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: first_target_policy.clone(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x240)),
- },
- &signer,
- )
- .await
- .expect("first enqueue");
- let replay = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: first_target_policy.clone(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x240)),
- },
- &signer,
- )
- .await
- .expect("replay enqueue");
- let conflict = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: second_target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x240)),
- },
- &signer,
- )
- .await
- .expect_err("conflicting command hash");
-
- assert_eq!(
- first.state,
- radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted
- );
- assert_eq!(
- replay.state,
- radroots_outbox::RadrootsOutboxEnqueueStatus::Inserted
- );
- assert_eq!(first.outbox_operation_id, replay.outbox_operation_id);
- assert_eq!(first.outbox_event_id, replay.outbox_event_id);
- assert_eq!(
- first.idempotency_digest_prefix,
- replay.idempotency_digest_prefix
- );
- assert!(matches!(
- conflict,
- RadrootsSdkError::IdempotencyConflict { .. }
- ));
- let plans = sdk
- ._outbox
- .delivery_plans(first.outbox_event_id)
- .await
- .expect("delivery plans");
- assert_eq!(plans.len(), 1);
- let targets = sdk
- ._outbox
- .delivery_targets(first.outbox_event_id)
- .await
- .expect("delivery targets");
- let target_uris = targets
- .iter()
- .map(|target| target.endpoint_uri.as_str())
- .collect::<std::collections::BTreeSet<_>>();
- assert_eq!(
- target_uris,
- std::collections::BTreeSet::from(["wss://relay-a.example.com"])
- );
- let recovery_count: i64 = sqlx::query(
- "SELECT COUNT(*) FROM sdk_runtime_recovery_receipt WHERE recovery_code = 'idempotency_conflict'",
- )
- .fetch_one(sdk._event_store.pool())
- .await
- .expect("recovery count")
- .try_get(0)
- .expect("recovery count value");
- assert_eq!(recovery_count, 1);
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_profile() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_013,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let signer = WorkflowSigner::new();
- let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-no-wait");
-
- let receipt = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::NoWait,
- idempotency_key: Some(workflow_idempotency_key(0x241)),
- },
- &signer,
- )
- .await
- .expect("no-wait enqueue");
-
- let event = sdk
- ._outbox
- .get_event(receipt.outbox_event_id)
- .await
- .expect("event")
- .expect("event");
- let plans = sdk
- ._outbox
- .delivery_plans(receipt.outbox_event_id)
- .await
- .expect("plans");
- let targets = sdk
- ._outbox
- .delivery_targets(receipt.outbox_event_id)
- .await
- .expect("targets");
-
- assert_eq!(
- event.state,
- radroots_outbox::RadrootsOutboxEventState::Published
- );
- assert_eq!(plans.len(), 1);
- assert_eq!(
- plans[0].satisfaction_policy,
- radroots_transport::RadrootsTransportSatisfactionPolicy::no_wait()
- );
- assert_ne!(
- plans[0].satisfaction_policy,
- radroots_transport::RadrootsTransportSatisfactionPolicy::all_accepted()
- );
- assert_eq!(plans[0].required_success_count, 0);
- assert_eq!(
- plans[0].status,
- radroots_outbox::RadrootsOutboxDeliveryPlanStatus::Complete
- );
- assert!(targets.is_empty());
- assert!(
- sdk._outbox
- .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_rejects_missing_explicit_idempotency_key_without_mutation() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_013,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-missing-idempotency");
-
- let error = match enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::NoWait,
- idempotency_key: None,
- },
- &WorkflowSigner::new(),
- )
- .await
- {
- Err(error) => error,
- Ok(_) => panic!("expected missing idempotency error"),
- };
-
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- assert_eq!(
- sdk._outbox
- .status_summary(0)
- .await
- .expect("outbox status")
- .total_events,
- 0
- );
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_conflicts() {
- let sdk = crate::RadrootsClient::builder()
- .transport_profile(nostr_profile("wss://relay.example.com"))
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_010,
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let signer = WorkflowSigner::new();
- let first_draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-success");
- let idempotency_key =
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency");
- let receipt = enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &first_draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(idempotency_key.clone()),
- },
- &signer,
- )
- .await
- .expect("enqueue signed workflow");
-
- assert_eq!(
- receipt.signed_event_id.to_hex(),
- first_draft.expected_event_id_hex()
- );
- assert!(receipt.local_event_seq > 0);
- assert!(receipt.outbox_operation_id > 0);
- assert!(receipt.outbox_event_id > 0);
- assert_eq!(receipt.idempotency_digest_prefix.len(), 12);
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 1
- );
- assert_eq!(
- sdk._outbox
- .status_summary(i64::MAX)
- .await
- .expect("outbox summary")
- .total_events,
- 1
- );
-
- let second_draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-conflict");
- let error = match enqueue_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &second_draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(idempotency_key),
- },
- &signer,
- )
- .await
- {
- Err(error) => error,
- Ok(_) => panic!("expected idempotency conflict"),
- };
-
- assert!(matches!(
- error,
- RadrootsSdkError::IdempotencyConflict {
- operation_kind,
- ..
- } if operation_kind == "farm.publish.v1"
- ));
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 1
- );
- assert_eq!(
- sdk._outbox
- .status_summary(i64::MAX)
- .await
- .expect("outbox summary")
- .total_events,
- 1
- );
-}
-
-#[cfg(feature = "signer-adapters")]
-#[tokio::test]
-async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() {
- let sdk = crate::RadrootsClient::builder()
- .transport_profile(nostr_profile("wss://relay.example.com"))
- .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
- 1_700_000_011,
- ))
- .signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_signer(WorkflowSigner::new(), farmer_pubkey())
- .expect("local signer"),
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-configured");
-
- let receipt = enqueue_configured_signed_workflow(
- &sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x242)),
- },
- )
- .await
- .expect("configured enqueue");
-
- assert_eq!(
- receipt.signed_event_id.to_hex(),
- draft.expected_event_id_hex()
- );
- assert_eq!(receipt.idempotency_digest_prefix.len(), 12);
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation() {
- let sdk = crate::RadrootsClient::builder()
- .transport_profile(nostr_profile("wss://relay.example.com"))
- .build()
- .await
- .expect("sdk");
- assert_eq!(
- sdk._event_store
- .status_summary()
- .await
- .expect("event store summary")
- .total_events,
- 0
- );
- sdk._outbox.pool().close().await;
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for(farmer_pubkey());
- let request = SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x243)),
- };
-
- let error = match enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await {
- Err(error) => error,
- Ok(_) => panic!("expected closed outbox error"),
- };
-
- assert!(matches!(error, RadrootsSdkError::EventStore { .. }));
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_reports_store_failures() {
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for(farmer_pubkey());
- let closed_store_sdk = crate::RadrootsClient::builder()
- .transport_profile(nostr_profile("wss://relay.example.com"))
- .build()
- .await
- .expect("sdk");
- closed_store_sdk._event_store.pool().close().await;
- let store_failure_request = SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x244)),
- };
- assert!(matches!(
- enqueue_signed_workflow(
- &closed_store_sdk,
- store_failure_request,
- &WorkflowSigner::new()
- )
- .await,
- Err(RadrootsSdkError::EventStore { .. })
- ));
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_reports_clock_failures() {
- let sdk = crate::RadrootsClient::builder()
- .clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
- .transport_profile(nostr_profile("wss://relay.example.com"))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for(farmer_pubkey());
- let request = SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::default_profile(),
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x245)),
- };
- assert!(matches!(
- enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await,
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
-}
-
-#[tokio::test]
-async fn enqueue_signed_workflow_rejects_transport_profile_targets_without_radrootsd_execution() {
- let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let draft = frozen_draft_for(farmer_pubkey());
- let request = SdkWorkflowEnqueueRequest {
- operation_kind: "farm.publish.v1",
- actor: &actor,
- frozen_draft: &draft,
- target_policy: TargetPolicy::DefaultProfile,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key: Some(workflow_idempotency_key(0x246)),
- };
-
- assert!(matches!(
- enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await,
- Err(RadrootsSdkError::EmptyTransportTargets { operation })
- if operation == "publish transport profile"
- ));
-}
diff --git a/docs/implementation/COMPATIBILITY_SHIMS.md b/docs/implementation/COMPATIBILITY_SHIMS.md
@@ -7,8 +7,6 @@ second protocol authority.
| Shim | Final owner | Remaining consumers | Final removal |
| --- | --- | --- | --- |
| `radroots_runtime_contract_v1` | `radroots_protocol::runtime::v1` | SDK CLI-host generator; standalone `oss/cli` runtime registry and command code | Step 270 |
-| SDK signer provider façade and `adapters::signer` | `radroots_signing` plus host-owned `radroots_nostr_connect` adapters | SDK runtime/examples/tests; standalone `oss/cli` and `oss/studio_app` | Step 313, after SDK Step 248, downstream Steps 269-293, and matrix Step 294 |
-| hidden `RadrootsSdkNip46ClientKey`, `RadrootsSdkNip46Transport`, and legacy constructors | package-owned `radroots_nostr_connect::client::{Client, Transport}` consumed by `RadrootsSdkMycNip46Signer::from_client` | standalone `oss/cli` | Step 313, after CLI NIP-46 cutover Step 271 and matrix Step 294 |
The `radroots_sdk` library no longer depends on or reexports the runtime shim.
Its radrootsd execution path also consumes
@@ -22,10 +20,11 @@ standalone `oss/radrootsd` and the runtime/protocol shims in standalone
`oss/cli`; those consumers are assigned to Steps 286 and 270 respectively.
No new consumer may be added before those cutovers.
-Step 109's first-party source search also found that immediate removal of the
-SDK-prefixed signer provider types would break the standalone CLI and Studio
-repositories. The SDK crate remains `publish = false`; the retained root
-reexports and signer adapter module are hidden from generated documentation.
-They must not gain new consumers or behavior. Step 248 owns the SDK-internal
-cutover, Step 294 must prove the downstream cutovers, and Step 313 removes the
-feature, dependency, module, reexports, and old names in full.
+Step 248 removed the inactive SDK signer adapter, Nostr adapter, prefixed
+models, private store, workflow runtime, and their dormant tests. No SDK-local
+compatibility alias, feature, module, or dependency remains for them. The
+standalone CLI still names predecessor SDK types and features, but that is an
+external red consumer rather than authority to retain a second SDK surface:
+Steps 269-272 own its dependency, product API, signing, and sync migrations;
+Step 294 proves the downstream matrix; Step 313 rejects any surviving legacy
+name before release qualification.
diff --git a/docs/implementation/SDK_SUPERSEDED_SURFACE_AUDIT.md b/docs/implementation/SDK_SUPERSEDED_SURFACE_AUDIT.md
@@ -0,0 +1,58 @@
+# SDK superseded-surface audit
+
+Step 248 closes the predecessor `radroots_sdk` source boundary without adding
+a deprecation package or compatibility API.
+
+## Reachability and manifest audit
+
+The final crate root reaches only the private `adapters::radrootsd` module and
+the eleven chartered public modules. Cargo metadata and the package manifest
+register exactly three integration tests and two examples. The removed files
+were not reachable from that module graph and were not registered targets
+because the package deliberately uses `autotests = false` and `autoexamples =
+false`.
+
+The deletion covers the dormant actor JSON, GeoNames wrapper, idempotency
+wrapper, identity/knowledge reexports and builders, privacy model, private SQL
+store, product-client wrappers, workflow runtime, obsolete Nostr/signer
+adapters, and all tests/support files that exercised only those sources. The
+active daemon adapter and its unit tests remain private because the chartered
+`transport::DaemonDelivery` implementation uses them.
+
+The final SDK manifest contains only the nine required and seven optional
+Radroots dependencies from the package charter. It has no predecessor private
+package dependency, production sibling path, prefixed feature alias, or
+unregistered compatibility target.
+
+## First-party consumer search
+
+The search covered executable/configuration source in the parent workspace and
+all checked-out `oss/*` capsules, excluding historical baselines, handoff
+evidence, generated API snapshots, build outputs, and the untracked historical
+`.sdk_step064_worktree` recovery checkout. That checkout is not a canonical
+repository input and was left untouched.
+
+One canonical external consumer remains red: standalone `oss/cli` still uses
+the sibling `../sdk/crates/sdk` path, retired feature names, and prefixed SDK
+types. This does not justify restoring a shim because the final SDK surface is
+already cut over and the CLI is not part of this standalone workspace. Its
+ordered disposition is:
+
+- Step 269 removes sibling paths and selects final package/features.
+- Step 270 migrates product operations and error imports.
+- Step 271 migrates signer and NIP-46 composition.
+- Step 272 migrates inbound/outbound synchronization.
+- Step 294 proves the complete downstream compatibility matrix.
+- Step 313 rejects all remaining legacy public names before qualification.
+
+No other checked-out first-party executable source imports prefixed SDK types.
+Documentation, release contracts, architecture fixtures, and lower-package
+READMEs that name the final `radroots_sdk` identity are intentional and are not
+compatibility consumers.
+
+## Release disposition
+
+`radroots_sdk` remains `publish = false`. No deprecation placeholder exists.
+The only retained compatibility package in this repository is the separately
+classified, non-publishable `radroots_runtime_contract_v1` generator bridge;
+it is not linked by the SDK library and its external CLI cutover is Step 270.
diff --git a/docs/implementation/SDK_WORKSPACE_CONSUMERS.md b/docs/implementation/SDK_WORKSPACE_CONSUMERS.md
@@ -16,12 +16,15 @@ intentional:
repositories/checkouts. Their ordered migrations remain assigned to Steps
269–294 and are not folded into this standalone repository step.
-The package-owned predecessor examples and inactive integration sources under
-`crates/sdk` are not Cargo consumers because the manifest sets `autotests =
-false` and `autoexamples = false`. They remain quarantined only until the
-ordered documentation and package-surface cleanup in Steps 247–248; they are
-not compiled, exported, or authorized as compatibility APIs.
+The package-owned predecessor examples were replaced in Step 247. Step 248
+removed every inactive predecessor module, integration source, support module,
+and unit-test source after proving they were outside the crate root and the
+manifest's explicit test/example target set. They are no longer present as a
+latent source boundary or compatibility API.
No compatibility alias or feature was added for an external consumer. The
next consumer edge created in this repository must be the exact `radroots ->
radroots_sdk` dependency and forwarding contract specified for the facade.
+
+The complete source search and downstream disposition are recorded in
+[`SDK_SUPERSEDED_SURFACE_AUDIT.md`](SDK_SUPERSEDED_SURFACE_AUDIT.md).
diff --git a/docs/implementation/deviations.toml b/docs/implementation/deviations.toml
@@ -13,16 +13,16 @@ spec_anchors = [
]
source_evidence = [
"Step 179, not Step 153, owns transfer of the current SDK private database and its encrypted records into canonical SQLite storage.",
- "The SDK can activate the final radroots_secrets dependency now while its private_store module remains the sole predecessor secret-store quarantine.",
+ "The SDK activated the final radroots_secrets dependency while its private_store module remained the sole predecessor secret-store quarantine through Step 247.",
"Mixed publish-frozen runtime, Myc, and other external hosts still require predecessor vault/store behavior until their ordered migration steps.",
]
-replacement_action = "Activate the final optional radroots_secrets edge in Step 153; confine predecessor vault/store imports to private_store.rs; Step 179 transfers canonical private storage, Steps 226/288/293 migrate the remaining SDK and downstream consumers, and Step 313 removes every remaining compatibility package and legacy name."
+replacement_action = "Activate the final optional radroots_secrets edge in Step 153; remove the SDK private_store quarantine at Step 248; Steps 288/293 migrate remaining downstream consumers, and Step 313 removes every remaining compatibility package and external legacy name."
verification = [
- "SDK source-boundary tests confine predecessor imports to private_store.rs and require the final optional radroots_secrets dependency edge.",
+ "Step 248 source reachability and manifest-target checks prove private_store.rs and its dormant tests are removed from the SDK package.",
"The local-signer feature activates radroots_secrets without changing the current runtime storage implementation before Step 179.",
"Step 155 release-policy validation keeps every quarantine package non-publishable until its exact removal gate.",
]
-unresolved_risk = "Publish-frozen compatibility code remains reachable inside the SDK private-store path until Steps 179, 226, 288, 293, and 313; no package-realistic publication may proceed while it remains."
+unresolved_risk = "Publish-frozen compatibility code remains in separate downstream capsules until Steps 288, 293, and 313; no package-realistic publication may proceed while it remains."
normative_architecture_change = false
adr_required = false