sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

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:
Dcrates/sdk/src/actor_json.rs | 62--------------------------------------------------------------
Dcrates/sdk/src/adapters/nostr.rs | 73-------------------------------------------------------------------------
Dcrates/sdk/src/adapters/signer.rs | 24------------------------
Dcrates/sdk/src/geonames.rs | 142-------------------------------------------------------------------------------
Dcrates/sdk/src/idempotency.rs | 118-------------------------------------------------------------------------------
Dcrates/sdk/src/identity.rs | 4----
Dcrates/sdk/src/knowledge.rs | 1324-------------------------------------------------------------------------------
Dcrates/sdk/src/privacy.rs | 181-------------------------------------------------------------------------------
Dcrates/sdk/src/private_store.rs | 1065-------------------------------------------------------------------------------
Dcrates/sdk/src/product_clients.rs | 80-------------------------------------------------------------------------------
Dcrates/sdk/src/workflow_runtime.rs | 1097-------------------------------------------------------------------------------
Dcrates/sdk/tests/geonames.rs | 229-------------------------------------------------------------------------------
Dcrates/sdk/tests/identity_public_api.rs | 33---------------------------------
Dcrates/sdk/tests/identity_retired_surface.rs | 118-------------------------------------------------------------------------------
Dcrates/sdk/tests/knowledge_public_api.rs | 783-------------------------------------------------------------------------------
Mcrates/sdk/tests/package_boundary.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/sdk/tests/replica_ingest.rs | 89-------------------------------------------------------------------------------
Dcrates/sdk/tests/secrets_migration_boundary.rs | 62--------------------------------------------------------------
Dcrates/sdk/tests/source_boundary.rs | 326-------------------------------------------------------------------------------
Dcrates/sdk/tests/support/fixture_signer.rs | 69---------------------------------------------------------------------
Dcrates/sdk/tests/support/serializer_failure.rs | 274-------------------------------------------------------------------------------
Dcrates/sdk/tests/unit/actor_json_tests.rs | 76----------------------------------------------------------------------------
Dcrates/sdk/tests/unit/adapters_nostr_tests.rs | 85-------------------------------------------------------------------------------
Dcrates/sdk/tests/unit/error_tests.rs | 573-------------------------------------------------------------------------------
Dcrates/sdk/tests/unit/idempotency_tests.rs | 90-------------------------------------------------------------------------------
Dcrates/sdk/tests/unit/private_store_tests.rs | 290-------------------------------------------------------------------------------
Dcrates/sdk/tests/unit/workflow_runtime_tests.rs | 928-------------------------------------------------------------------------------
Mdocs/implementation/COMPATIBILITY_SHIMS.md | 17++++++++---------
Adocs/implementation/SDK_SUPERSEDED_SURFACE_AUDIT.md | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdocs/implementation/SDK_WORKSPACE_CONSUMERS.md | 13++++++++-----
Mdocs/implementation/deviations.toml | 8++++----
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