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:
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,