cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 9034f1596b81695c1f3491754834aa30a10b3c55
parent 6c2923dbfaff0fdd3ac4718713467c1f705484f4
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Jul 2026 11:45:30 +0000

cli: enforce strict Profile publication boundaries

- replace serializable legacy Profile models with CLI-owned farm draft and view DTOs
- author tagless kind-0 previews through typed metadata and reject unverified Blossom media
- remove false kind-0 sync signer readiness and align NIP-46 client interoperability
- update bounded logging, lockfiles, and release tests for the 1.0.0-alpha.1 cohort

Diffstat:
MCargo.lock | 85+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mflake.lock | 6+++---
Msrc/runtime/farm.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/runtime/farm_config.rs | 29++++++++++++++++++++++++++---
Msrc/runtime/logging.rs | 8++++++--
Msrc/runtime/sdk.rs | 2+-
Msrc/runtime/signer.rs | 31++++++++-----------------------
Msrc/runtime/sync.rs | 55+++++++++++++++++++++++++++++++++----------------------
Msrc/view/runtime.rs | 26++++++++++++++++++++++++--
9 files changed, 321 insertions(+), 98 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1335,6 +1335,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] name = "js-sys" version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1484,6 +1490,12 @@ dependencies = [ ] [[package]] +name = "mediatype" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120fa187be19d9962f0926633453784691731018a2bf936ddb4e29101b79c4a7" + +[[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1992,13 +2004,23 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radroots_authority" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_event", "radroots_nostr", ] [[package]] +name = "radroots_blossom" +version = "1.0.0-alpha.1" +dependencies = [ + "mediatype", + "serde", + "sha2", + "url", +] + +[[package]] name = "radroots_cli" version = "0.1.0" dependencies = [ @@ -2049,7 +2071,7 @@ dependencies = [ [[package]] name = "radroots_core" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "rust_decimal", "rust_decimal_macros", @@ -2058,19 +2080,22 @@ dependencies = [ [[package]] name = "radroots_event" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", + "jiff-tzdb", + "radroots_blossom", "radroots_core", "secp256k1", "serde", "serde_json", "sha2", + "url", ] [[package]] name = "radroots_event_codec" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", "radroots_core", @@ -2081,7 +2106,7 @@ dependencies = [ [[package]] name = "radroots_event_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_event", @@ -2096,7 +2121,7 @@ dependencies = [ [[package]] name = "radroots_geocoder" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures-executor", "hex", @@ -2112,10 +2137,9 @@ dependencies = [ [[package]] name = "radroots_identity" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", - "radroots_event", "radroots_protected_store", "radroots_runtime", "radroots_runtime_paths", @@ -2128,9 +2152,10 @@ dependencies = [ [[package]] name = "radroots_log" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chrono", + "serde_json", "thiserror 1.0.69", "tracing", "tracing-appender", @@ -2139,14 +2164,14 @@ dependencies = [ [[package]] name = "radroots_mesh" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_transport", ] [[package]] name = "radroots_nostr" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", "nostr-sdk", @@ -2160,7 +2185,7 @@ dependencies = [ [[package]] name = "radroots_nostr_accounts" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_identity", "radroots_nostr_signer", @@ -2175,7 +2200,7 @@ dependencies = [ [[package]] name = "radroots_nostr_connect" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", "serde", @@ -2186,7 +2211,7 @@ dependencies = [ [[package]] name = "radroots_nostr_signer" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "nostr", @@ -2204,7 +2229,7 @@ dependencies = [ [[package]] name = "radroots_outbox" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_event", @@ -2219,7 +2244,7 @@ dependencies = [ [[package]] name = "radroots_protected_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chacha20poly1305", "getrandom 0.2.17", @@ -2231,14 +2256,14 @@ dependencies = [ [[package]] name = "radroots_protocol_contract_v1" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", ] [[package]] name = "radroots_replica_schema" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -2246,7 +2271,7 @@ dependencies = [ [[package]] name = "radroots_replica_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_replica_schema", @@ -2258,7 +2283,7 @@ dependencies = [ [[package]] name = "radroots_replica_sync" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "hex", @@ -2276,7 +2301,7 @@ dependencies = [ [[package]] name = "radroots_runtime" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "anyhow", "chacha20poly1305", @@ -2306,7 +2331,7 @@ dependencies = [ [[package]] name = "radroots_runtime_paths" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "thiserror 1.0.69", @@ -2314,7 +2339,7 @@ dependencies = [ [[package]] name = "radroots_runtime_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_sql_core", "serde", @@ -2360,14 +2385,14 @@ dependencies = [ [[package]] name = "radroots_secret_vault" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "keyring", ] [[package]] name = "radroots_sql_core" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chrono", "futures-executor", @@ -2379,7 +2404,7 @@ dependencies = [ [[package]] name = "radroots_trade" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "hex", @@ -2397,7 +2422,7 @@ dependencies = [ [[package]] name = "radroots_transport" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "sha2", @@ -2405,7 +2430,7 @@ dependencies = [ [[package]] name = "radroots_transport_nostr" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "nostr", @@ -2423,7 +2448,7 @@ dependencies = [ [[package]] name = "radroots_transport_publish_protocol" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_transport", "serde", @@ -2431,7 +2456,7 @@ dependencies = [ [[package]] name = "radroots_transport_reticulum" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_transport", ] diff --git a/flake.lock b/flake.lock @@ -29,11 +29,11 @@ ] }, "locked": { - "lastModified": 1775445266, - "narHash": "sha256-3fgIj85WHQbOamrpIw9WY3ZL1PoEvjPOjmzMYNsEQJo=", + "lastModified": 1784350408, + "narHash": "sha256-OstzLWL5t7Xe14xEC6GIMJCp0PrYNTSA0El7GG2av88=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "61747bc3cf2da179b9af356ae9e70f3b895b0c24", + "rev": "3c38e1e1ba9c8d7030f7b5a801398ea7d8a6fdc0", "type": "github" }, "original": { diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -7,9 +7,9 @@ use radroots_event::farm::{RadrootsFarm, RadrootsFarmPublicLocation}; use radroots_event::ids::RadrootsAddressableCoordinate; use radroots_event::kinds::{KIND_FARM, KIND_PROFILE}; use radroots_event::listing::RadrootsListingPublicLocation; -use radroots_event::profile::{RadrootsProfile, RadrootsProfileType}; +use radroots_event::profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier}; use radroots_event_codec::d_tag::is_d_tag_base64url; -use radroots_event_codec::profile::encode::to_wire_parts_with_profile_type; +use radroots_event_codec::profile::authored::authored_profile_to_wire_parts; use radroots_sdk::{ FarmEnqueuePublishRequest, FarmEnqueueReceipt, FarmPreparePublishRequest, FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate, @@ -30,7 +30,8 @@ use crate::runtime::account::{self, AccountRecordView}; use crate::runtime::config::{RuntimeConfig, SignerBackend, TransportProfileKind}; use crate::runtime::farm_config::{ self, FarmConfigDocument, FarmConfigScope, FarmConfigSelection, FarmListingDefaults, - FarmMissingField, FarmPublicationStatus, ResolvedFarmConfig, SUPPORTED_FARM_CONFIG_VERSION, + FarmMissingField, FarmProfileDraft, FarmPublicationStatus, ResolvedFarmConfig, + SUPPORTED_FARM_CONFIG_VERSION, }; use crate::runtime::runtime_store::append_local_work; use crate::runtime::sdk::{ @@ -41,9 +42,9 @@ use crate::runtime::signer::ActorWriteBindingError; use crate::view::runtime::{ FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView, FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView, - FarmPrivatePublicLocalityView, FarmPublicationView, FarmPublishComponentView, - FarmPublishEventView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView, - FarmSetupView, FarmStatusView, TransportTargetFailureView, + FarmPrivatePublicLocalityView, FarmProfileDraftView, FarmPublicationView, + FarmPublishComponentView, FarmPublishEventView, FarmPublishView, FarmRebindView, + FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView, TransportTargetFailureView, }; const FARM_CONFIG_SOURCE: &str = "farm config ยท local first"; @@ -957,9 +958,10 @@ fn build_publish_previews( document: &FarmConfigDocument, account_pubkey: &str, ) -> Result<FarmPublishPreviews, RuntimeError> { - let profile_parts = - to_wire_parts_with_profile_type(&document.profile, Some(RadrootsProfileType::Farm)) - .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?; + require_verified_publish_media(&document.profile, &document.farm)?; + let profile = authored_profile_from_draft(&document.profile)?; + let profile_parts = authored_profile_to_wire_parts(&profile) + .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?; Ok(FarmPublishPreviews { profile: FarmPublishEventDraft { @@ -975,6 +977,60 @@ fn build_publish_previews( }) } +fn authored_profile_from_draft( + draft: &FarmProfileDraft, +) -> Result<RadrootsAuthoredProfile, RuntimeError> { + let mut profile = RadrootsAuthoredProfile::new(draft.name.clone()) + .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?; + if let Some(display_name) = draft.display_name.as_deref().and_then(non_empty) { + profile = profile.with_display_name(display_name); + } + if let Some(about) = draft.about.as_deref().and_then(non_empty) { + profile = profile.with_about(about); + } + if let Some(nip05) = draft.nip05.as_deref().and_then(non_empty) { + let identifier = RadrootsNip05Identifier::parse(nip05.as_str()).map_err(|error| { + RuntimeError::Config(format!("invalid farm profile NIP-05 identifier: {error}")) + })?; + profile = profile.with_nip05(identifier); + } + if let Some(bot) = draft.bot.as_deref().and_then(non_empty) { + let bot = match bot.as_str() { + "true" => true, + "false" => false, + _ => { + return Err(RuntimeError::Config( + "farm profile bot must be `true` or `false`".to_owned(), + )); + } + }; + profile = profile.with_bot(bot); + } + Ok(profile) +} + +fn require_verified_publish_media( + profile: &FarmProfileDraft, + farm: &RadrootsFarm, +) -> Result<(), RuntimeError> { + let contains_unverified_media = [ + profile.picture.as_deref(), + profile.banner.as_deref(), + farm.picture.as_deref(), + farm.banner.as_deref(), + ] + .into_iter() + .flatten() + .any(|value| !value.trim().is_empty()); + if contains_unverified_media { + return Err(RuntimeError::Config( + "farm publish cannot use raw picture or banner URLs; publish media only after byte-verifying the Blossom descriptor and retaining proof of successful BUD-02 upload completion" + .to_owned(), + )); + } + Ok(()) +} + fn component_idempotency_key( args: &FarmPublishArgs, component: &str, @@ -1695,7 +1751,7 @@ fn init_document( account: account.record.account_id.to_string(), farm_d_tag: farm_d_tag.clone(), }, - profile: RadrootsProfile { + profile: FarmProfileDraft { name: name.clone(), display_name, nip05: None, @@ -2060,7 +2116,18 @@ fn document_view(document: &FarmConfigDocument) -> FarmConfigDocumentView { seller_account_id: document.selection.account.clone(), farm_d_tag: document.selection.farm_d_tag.clone(), }, - profile: document.profile.clone(), + profile: FarmProfileDraftView { + name: document.profile.name.clone(), + display_name: document.profile.display_name.clone(), + nip05: document.profile.nip05.clone(), + about: document.profile.about.clone(), + website: document.profile.website.clone(), + picture: document.profile.picture.clone(), + banner: document.profile.banner.clone(), + lud06: document.profile.lud06.clone(), + lud16: document.profile.lud16.clone(), + bot: document.profile.bot.clone(), + }, farm: document.farm.clone(), listing_defaults: FarmListingDefaultsView { delivery_method: document.listing_defaults.delivery_method.clone(), @@ -2308,11 +2375,97 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String { #[cfg(test)] mod tests { - use super::generate_d_tag; + use super::{authored_profile_from_draft, generate_d_tag, require_verified_publish_media}; + use crate::runtime::RuntimeError; + use crate::runtime::farm_config::FarmProfileDraft; + use radroots_event::farm::RadrootsFarm; use radroots_event_codec::d_tag::is_d_tag_base64url; + use radroots_event_codec::profile::authored::authored_profile_to_wire_parts; + use serde_json::json; #[test] fn generated_farm_d_tag_is_valid_base64url() { assert!(is_d_tag_base64url(&generate_d_tag())); } + + #[test] + fn farm_profile_draft_uses_strict_tagless_profile_authoring() { + let draft = FarmProfileDraft { + name: "moss street farm".to_owned(), + display_name: Some("Moss Street Farm".to_owned()), + nip05: Some("farm@example.com".to_owned()), + about: Some("Victoria produce".to_owned()), + website: Some("https://example.com".to_owned()), + bot: Some("true".to_owned()), + ..FarmProfileDraft::default() + }; + + let authored = authored_profile_from_draft(&draft).expect("strict authored Profile"); + let wire = authored_profile_to_wire_parts(&authored).expect("Profile wire parts"); + + assert!(wire.tags.is_empty()); + assert_eq!( + serde_json::from_str::<serde_json::Value>(&wire.content).expect("Profile metadata"), + json!({ + "name": "moss street farm", + "display_name": "Moss Street Farm", + "about": "Victoria produce", + "nip05": "farm@example.com", + "bot": true, + }) + ); + } + + #[test] + fn farm_profile_draft_rejects_non_boolean_legacy_bot_values() { + let draft = FarmProfileDraft { + name: "moss street farm".to_owned(), + bot: Some("yes".to_owned()), + ..FarmProfileDraft::default() + }; + + let RuntimeError::Config(message) = + authored_profile_from_draft(&draft).expect_err("invalid bot must fail closed") + else { + panic!("expected config error"); + }; + assert_eq!(message, "farm profile bot must be `true` or `false`"); + } + + #[test] + fn farm_publish_rejects_raw_media_without_blossom_proofs() { + let mut draft = FarmProfileDraft { + name: "moss street farm".to_owned(), + picture: Some("https://media.example/picture.jpg".to_owned()), + ..FarmProfileDraft::default() + }; + let mut farm = sample_farm(); + + assert_blossom_proof_error(require_verified_publish_media(&draft, &farm)); + + draft.picture = None; + farm.banner = Some("https://media.example/banner.jpg".to_owned()); + assert_blossom_proof_error(require_verified_publish_media(&draft, &farm)); + } + + fn sample_farm() -> RadrootsFarm { + RadrootsFarm { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), + name: "Moss Street Farm".to_owned(), + about: None, + website: None, + picture: None, + banner: None, + location: None, + tags: None, + } + } + + fn assert_blossom_proof_error(result: Result<(), RuntimeError>) { + let RuntimeError::Config(message) = result.expect_err("raw media must fail closed") else { + panic!("expected config error"); + }; + assert!(message.contains("Blossom descriptor")); + assert!(message.contains("BUD-02 upload completion")); + } } diff --git a/src/runtime/farm_config.rs b/src/runtime/farm_config.rs @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf}; use radroots_event::farm::RadrootsFarm; use radroots_event::listing::{RadrootsListingDeliveryMethod, RadrootsListingPublicLocation}; -use radroots_event::profile::RadrootsProfile; use radroots_event_codec::d_tag::is_d_tag_base64url; use serde::{Deserialize, Serialize}; @@ -34,13 +33,33 @@ impl FarmConfigScope { pub struct FarmConfigDocument { pub version: u32, pub selection: FarmConfigSelection, - pub profile: RadrootsProfile, + pub profile: FarmProfileDraft, pub farm: RadrootsFarm, pub listing_defaults: FarmListingDefaults, #[serde(default)] pub publication: FarmPublicationStatus, } +/// CLI-owned local draft for a future kind-0 Profile replacement. +/// +/// String media references are deliberately retained only as local draft data. +/// They are not proof of Blossom byte verification or BUD-02 upload completion +/// and therefore cannot cross the Profile publication boundary. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FarmProfileDraft { + pub name: String, + pub display_name: Option<String>, + pub nip05: Option<String>, + pub about: Option<String>, + pub website: Option<String>, + pub picture: Option<String>, + pub banner: Option<String>, + pub lud06: Option<String>, + pub lud16: Option<String>, + pub bot: Option<String>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FarmConfigSelection { @@ -425,7 +444,7 @@ mod tests { account: "seller".to_owned(), farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), }, - profile: RadrootsProfile { + profile: FarmProfileDraft { name: "La Huerta".to_owned(), display_name: Some("La Huerta".to_owned()), nip05: None, @@ -526,6 +545,10 @@ mod tests { ); assert_eq!(resolved.document.farm.d_tag, "AAAAAAAAAAAAAAAAAAAAAA"); assert_eq!( + resolved.document.profile.website.as_deref(), + Some("https://example.invalid/la-huerta") + ); + assert_eq!( resolved.document.listing_defaults.location.primary, "San Francisco, CA" ); diff --git a/src/runtime/logging.rs b/src/runtime/logging.rs @@ -28,13 +28,14 @@ pub fn to_radroots_logging_options(config: &LoggingConfig) -> LoggingOptions { file_name: CLI_LOG_FILE_NAME.to_owned(), stdout: config.stdout, default_level: Some(config.filter.clone()), - file_layout: LogFileLayout::PrefixedDate, + file_layout: LogFileLayout::StableFileName, + ..LoggingOptions::default() } } #[cfg(test)] mod tests { - use super::to_radroots_logging_options; + use super::{LogFileLayout, to_radroots_logging_options}; use crate::runtime::config::LoggingConfig; use std::path::PathBuf; @@ -49,5 +50,8 @@ mod tests { assert_eq!(options.default_level.as_deref(), Some("info")); assert_eq!(options.dir, Some(PathBuf::from("logs"))); assert!(!options.stdout); + assert_eq!(options.file_layout, LogFileLayout::StableFileName); + assert!(options.rotation.max_file_bytes > 0); + assert!(options.rotation.retained_files > 0); } } diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -494,7 +494,7 @@ impl CliSdkNip46RelayTransport { "failed to build signer.remote_nip46 filter: {error}" )) })?; - let notifications = client.notifications(); + let notifications = client.clone().into_inner().notifications(); let subscribe_output = client.subscribe(filter, None).await.map_err(|error| { RuntimeError::Network(format!( "failed to subscribe to signer.remote_nip46 response relays: {error}" diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -10,8 +10,8 @@ use crate::view::runtime::{ SignerStatusView, SignerWriteKindReadinessView, }; use radroots_event::kinds::{ - KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, - KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, + KIND_FARM, KIND_LISTING, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL, + KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, }; use radroots_nostr_accounts::prelude::RadrootsNostrAccountStatus; use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; @@ -294,13 +294,9 @@ fn disabled_binding_status() -> SignerBindingStatusView { } } -fn cli_write_kinds() -> [CliWriteKind; 10] { +fn cli_write_kinds() -> [CliWriteKind; 9] { [ CliWriteKind { - command: "sync.push", - event_kind: KIND_PROFILE, - }, - CliWriteKind { command: "farm.publish", event_kind: KIND_FARM, }, @@ -586,9 +582,9 @@ fn sign_event_permission_for_kind(event_kind: u32) -> String { #[cfg(test)] mod tests { use super::{ - KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, - KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, - cli_write_kinds, myc_managed_account_ref_matches, myc_write_kind_readiness, + KIND_FARM, KIND_LISTING, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL, + KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, cli_write_kinds, + myc_managed_account_ref_matches, myc_write_kind_readiness, myc_write_kind_readiness_for_permissions, sign_event_permission_for_kind, }; use radroots_nostr_connect::prelude::{ @@ -605,7 +601,6 @@ mod tests { assert_eq!( commands, [ - "sync.push", "farm.publish", "listing.publish", "listing.update", @@ -618,6 +613,7 @@ mod tests { ] ); assert!(!commands.contains(&"signer.status")); + assert!(!commands.contains(&"sync.push")); } #[test] @@ -644,18 +640,7 @@ mod tests { #[test] fn myc_write_readiness_requires_exact_permissions() { let readiness = myc_write_kind_readiness(true, None); - let sync = readiness - .iter() - .find(|kind| kind.command == "sync.push") - .expect("sync readiness"); - - assert_eq!(sync.event_kind, KIND_PROFILE); - assert_eq!(sync.permission, "sign_event:0"); - assert!(!sync.ready); - assert_eq!( - sync.reason.as_deref(), - Some("SDK Myc signer permission is not configured for this event kind") - ); + assert!(readiness.iter().all(|kind| kind.command != "sync.push")); for (command, event_kind) in [ ("farm.publish", KIND_FARM), diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -1768,12 +1768,12 @@ mod tests { use radroots_event::list::RadrootsListEntry; use radroots_event::list_set::RadrootsListSet; use radroots_event::plot::RadrootsPlot; - use radroots_event::profile::{RadrootsProfile, RadrootsProfileType}; + use radroots_event::profile::RadrootsAuthoredProfile; use radroots_event::wire::RadrootsNip01EventWireParts; use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; - use radroots_event_codec::profile::encode as profile_encode; + use radroots_event_codec::profile::authored::authored_profile_to_wire_parts; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrTimestamp, radroots_nostr_build_event, @@ -2689,7 +2689,6 @@ mod tests { let seller_pubkey = seller.public_key_hex(); let listing_addr = format!("{KIND_LISTING}:{seller_pubkey}:{LISTING_D_TAG}"); let events = vec![ - profile_event(&seller), farm_event(&seller), plot_event(&seller), listing_event(&seller), @@ -2699,8 +2698,8 @@ mod tests { let view = pull_with_fetcher(&config, fake_fetcher(events)).expect("sync pull ingest"); assert_eq!(view.state, "ready"); - assert_eq!(view.fetched_count, Some(5)); - assert_eq!(view.ingested_count, Some(5)); + assert_eq!(view.fetched_count, Some(4)); + assert_eq!(view.ingested_count, Some(4)); assert_eq!(view.skipped_count, Some(0)); assert_eq!(view.unsupported_count, Some(0)); assert_eq!(view.failed_count, Some(0)); @@ -2732,6 +2731,30 @@ mod tests { } #[test] + fn sync_pull_keeps_tagless_profile_out_of_legacy_replica_projection() { + let dir = tempdir().expect("tempdir"); + let config = sample_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); + crate::runtime::store::init(&config).expect("store init"); + let seller = identity(17); + + let view = pull_with_fetcher(&config, fake_fetcher(vec![profile_event(&seller)])) + .expect("sync pull Profile"); + + assert_eq!(view.state, "ready"); + assert_eq!(view.fetched_count, Some(1)); + assert_eq!(view.ingested_count, Some(0)); + assert_eq!(view.skipped_count, Some(0)); + assert_eq!(view.unsupported_count, Some(0)); + assert_eq!(view.failed_count, Some(1)); + assert_eq!(view.reason_code.as_deref(), Some("sync_ingest_failed")); + assert!( + view.reason + .as_deref() + .is_some_and(|reason| reason.contains("profile_type required")) + ); + } + + #[test] fn market_refresh_uses_market_scope_for_ingest() { let dir = tempdir().expect("tempdir"); let config = sample_config(dir.path(), vec!["wss://relay.example.com".to_owned()]); @@ -2990,25 +3013,13 @@ mod tests { } fn profile_event(identity: &RadrootsIdentity) -> RadrootsNostrEvent { - let profile = RadrootsProfile { - name: "seller".to_owned(), - display_name: Some("Seller".to_owned()), - nip05: None, - about: Some("market seller".to_owned()), - website: Some("https://seller.example.com".to_owned()), - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; + let profile = RadrootsAuthoredProfile::new("seller") + .expect("profile") + .with_display_name("Seller") + .with_about("market seller"); signed_event( identity, - profile_encode::to_wire_parts_with_profile_type( - &profile, - Some(RadrootsProfileType::Farm), - ) - .expect("profile parts"), + authored_profile_to_wire_parts(&profile).expect("profile parts"), ) } diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -4,7 +4,6 @@ use radroots_event::farm::RadrootsFarm; use radroots_event::ids::RadrootsListingAddress; use radroots_event::kinds::KIND_LISTING; use radroots_event::listing::RadrootsListingPublicLocation; -use radroots_event::profile::RadrootsProfile; use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord; use serde::Serialize; @@ -1080,13 +1079,36 @@ pub struct FarmConfigSummaryView { #[derive(Debug, Clone, Serialize)] pub struct FarmConfigDocumentView { pub selection: FarmSelectionView, - pub profile: RadrootsProfile, + pub profile: FarmProfileDraftView, pub farm: RadrootsFarm, pub listing_defaults: FarmListingDefaultsView, pub publication: FarmPublicationView, } #[derive(Debug, Clone, Serialize)] +pub struct FarmProfileDraftView { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub nip05: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub about: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub website: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub picture: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub banner: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub lud06: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub lud16: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub bot: Option<String>, +} + +#[derive(Debug, Clone, Serialize)] pub struct FarmSelectionView { pub scope: String, pub seller_account_id: String,