lib

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

commit 773f20df3dfe57b6aecf8963561cd108f51a00c6
parent 9c5ab007f09c7f5cad996ba637c06dfc569c1f28
Author: triesap <tyson@radroots.org>
Date:   Tue,  4 Aug 2026 22:44:32 +0000

refactor(event): add validated generic authoring input

- add bounded contract identifiers versions and resolved keys
- revalidate private identity fields during serde reconstruction
- introduce a generic-only event draft with exact author binding
- reject every typed-only and read-only registry contract


Diffstat:
Mcrates/event/src/contract.rs | 345+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event/src/generic_draft.rs | 329+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event/src/lib.rs | 6+++---
Mcrates/event/tests/package_boundary.rs | 3+++
Mcrates/nostr/tests/authored_wire_corpus.rs | 8++++----
5 files changed, 684 insertions(+), 7 deletions(-)

diff --git a/crates/event/src/contract.rs b/crates/event/src/contract.rs @@ -1,5 +1,12 @@ #![forbid(unsafe_code)] +#[cfg(all(not(feature = "std"), not(test)))] +use alloc::{string::String, vec::Vec}; +#[cfg(any(feature = "std", test))] +use std::{string::String, vec::Vec}; + +use core::fmt; + /// Exact package version implementing this event contract surface. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -7,3 +14,341 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub mod registry_v7; pub use registry_v7::*; + +/// Maximum UTF-8 byte length of a versioned Radroots event-contract ID. +pub const CONTRACT_ID_MAX_BYTES: usize = 255; + +/// A syntactically valid, bounded Radroots event-contract identifier. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ContractId(String); + +impl ContractId { + pub fn parse(value: impl Into<String>) -> Result<Self, ContractIdentityError> { + let value = value.into(); + if value.is_empty() { + return Err(ContractIdentityError::ContractIdMissing); + } + if value.len() > CONTRACT_ID_MAX_BYTES { + return Err(ContractIdentityError::ContractIdTooLong { + max: CONTRACT_ID_MAX_BYTES, + actual: value.len(), + }); + } + if !value.starts_with("radroots.") { + return Err(ContractIdentityError::ContractIdNamespace); + } + let mut segments = value.split('.'); + let namespace = segments.next(); + let remaining = segments.collect::<Vec<_>>(); + let Some(version) = remaining.last() else { + return Err(ContractIdentityError::ContractIdSyntax); + }; + if namespace != Some("radroots") + || remaining.len() < 3 + || remaining[..remaining.len() - 1].iter().any(|segment| { + segment.is_empty() + || !segment.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' + }) + }) + || !version.strip_prefix('v').is_some_and(|number| { + !number.is_empty() + && !number.starts_with('0') + && number.bytes().all(|byte| byte.is_ascii_digit()) + }) + { + return Err(ContractIdentityError::ContractIdSyntax); + } + Ok(Self(value)) + } + + #[must_use] + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + #[must_use] + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for ContractId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A nonzero immutable event-contract registry version. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegistryVersion(u32); + +impl RegistryVersion { + pub const CURRENT: Self = Self(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION); + + pub const fn new(value: u32) -> Result<Self, ContractIdentityError> { + if value == 0 { + return Err(ContractIdentityError::RegistryVersionZero); + } + Ok(Self(value)) + } + + #[must_use] + pub const fn get(self) -> u32 { + self.0 + } +} + +impl fmt::Display for RegistryVersion { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}", self.get()) + } +} + +/// One event contract resolved against an immutable historical registry. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ContractKey { + registry_version: RegistryVersion, + contract_id: ContractId, +} + +impl ContractKey { + pub fn current(contract_id: impl Into<String>) -> Result<Self, ContractIdentityError> { + Self::new(RegistryVersion::CURRENT, ContractId::parse(contract_id)?) + } + + pub fn new( + registry_version: RegistryVersion, + contract_id: ContractId, + ) -> Result<Self, ContractIdentityError> { + if registry_version != RegistryVersion::CURRENT { + return Err(ContractIdentityError::UnsupportedRegistryVersion { + actual: registry_version.get(), + }); + } + if event_contract_registry_v7(contract_id.as_str()).is_none() { + return Err(ContractIdentityError::UnknownContract { + contract_id: contract_id.into_string(), + }); + } + Ok(Self { + registry_version, + contract_id, + }) + } + + #[must_use] + pub const fn registry_version(&self) -> RegistryVersion { + self.registry_version + } + + #[must_use] + pub const fn contract_id(&self) -> &ContractId { + &self.contract_id + } + + #[must_use] + pub fn contract(&self) -> &'static EventContract { + event_contract_registry_v7(self.contract_id.as_str()) + .expect("validated registry-v7 contract key must remain resolvable") + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ContractIdentityError { + ContractIdMissing, + ContractIdTooLong { max: usize, actual: usize }, + ContractIdNamespace, + ContractIdSyntax, + RegistryVersionZero, + UnsupportedRegistryVersion { actual: u32 }, + UnknownContract { contract_id: String }, +} + +impl ContractIdentityError { + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + Self::ContractIdMissing => "contract_id_missing", + Self::ContractIdTooLong { .. } => "contract_id_too_long", + Self::ContractIdNamespace => "contract_id_namespace", + Self::ContractIdSyntax => "contract_id_syntax", + Self::RegistryVersionZero => "registry_version_zero", + Self::UnsupportedRegistryVersion { .. } => "unsupported_registry_version", + Self::UnknownContract { .. } => "unknown_contract", + } + } +} + +impl fmt::Display for ContractIdentityError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ContractIdMissing => formatter.write_str("event contract ID must not be empty"), + Self::ContractIdTooLong { max, actual } => { + write!( + formatter, + "event contract ID is {actual} bytes; max is {max}" + ) + } + Self::ContractIdNamespace => { + formatter.write_str("event contract ID must use the `radroots.` namespace") + } + Self::ContractIdSyntax => { + formatter.write_str("event contract ID must be a canonical versioned identifier") + } + Self::RegistryVersionZero => { + formatter.write_str("event contract registry version must be nonzero") + } + Self::UnsupportedRegistryVersion { actual } => { + write!( + formatter, + "unsupported event contract registry version {actual}" + ) + } + Self::UnknownContract { contract_id } => { + write!(formatter, "unknown event contract `{contract_id}`") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContractIdentityError {} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for ContractId { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for ContractId { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::parse(value).map_err(serde::de::Error::custom) + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RegistryVersion { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_u32(self.get()) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RegistryVersion { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + Self::new(u32::deserialize(deserializer)?).map_err(serde::de::Error::custom) + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for ContractKey { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("ContractKey", 2)?; + state.serialize_field("registry_version", &self.registry_version)?; + state.serialize_field("contract_id", &self.contract_id)?; + state.end() + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for ContractKey { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct ContractKeySerde { + registry_version: RegistryVersion, + contract_id: ContractId, + } + + let value = ContractKeySerde::deserialize(deserializer)?; + Self::new(value.registry_version, value.contract_id).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod identity_tests { + use super::*; + + #[test] + fn contract_identity_values_are_bounded_canonical_and_resolved() { + let id = ContractId::parse("radroots.social.geochat.v1").expect("contract ID"); + assert_eq!(id.as_str(), "radroots.social.geochat.v1"); + assert_eq!(RegistryVersion::CURRENT.get(), 7); + let key = ContractKey::new(RegistryVersion::CURRENT, id).expect("contract key"); + assert_eq!(key.contract().id, "radroots.social.geochat.v1"); + + for invalid in [ + "", + "social.geochat.v1", + "radroots.social.Geochat.v1", + "radroots.social.geochat", + "radroots.social.geochat.v01", + "radroots..geochat.v1", + ] { + assert!(ContractId::parse(invalid).is_err(), "{invalid}"); + } + assert!(ContractId::parse(format!("radroots.{}.event.v1", "x".repeat(256))).is_err()); + assert_eq!( + RegistryVersion::new(0), + Err(ContractIdentityError::RegistryVersionZero) + ); + assert!(matches!( + ContractKey::new( + RegistryVersion::new(8).expect("nonzero version"), + ContractId::parse("radroots.social.geochat.v1").expect("contract ID") + ), + Err(ContractIdentityError::UnsupportedRegistryVersion { actual: 8 }) + )); + assert!(matches!( + ContractKey::current("radroots.social.unknown.v1"), + Err(ContractIdentityError::UnknownContract { .. }) + )); + } + + #[test] + fn serde_reconstruction_revalidates_identity_and_registry_membership() { + let key = ContractKey::current("radroots.social.geochat.v1").expect("contract key"); + let value = serde_json::to_value(&key).expect("key JSON"); + assert_eq!( + serde_json::from_value::<ContractKey>(value.clone()).expect("decoded key"), + key + ); + + let mut invalid = value.clone(); + invalid["registry_version"] = serde_json::json!(0); + assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); + let mut invalid = value.clone(); + invalid["registry_version"] = serde_json::json!(8); + assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); + let mut invalid = value.clone(); + invalid["contract_id"] = serde_json::json!("radroots.social.unknown.v1"); + assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); + let mut invalid = value; + invalid["unexpected"] = serde_json::json!(true); + assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); + } +} diff --git a/crates/event/src/generic_draft.rs b/crates/event/src/generic_draft.rs @@ -0,0 +1,329 @@ +//! Generic-only authored event input. + +#[cfg(all(not(feature = "std"), not(test)))] +use alloc::{borrow::ToOwned, string::String, vec::Vec}; +#[cfg(any(feature = "std", test))] +use std::{borrow::ToOwned, string::String, vec::Vec}; + +use radroots_identity::PublicKey; + +use crate::{ + contract::{ + ContractKey, EventAuthoringPolicy, RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, + validate_event_contract_parts, + }, + draft::DraftError, + envelope::{EventKind, EventTags, EventTimestamp}, + id::{EventId, parse_public_key}, + wire::{canonical_nip01_event_id_preimage, compute_canonical_nip01_event_id}, +}; + +/// A validated generic-only authored event input. +/// +/// Typed-only and read-only contracts cannot cross this boundary. Typed +/// authoring is owned by explicit codec conversions instead. +/// +/// ```compile_fail +/// use radroots_event::{GenericEventDraft, post::AuthoredUpdate}; +/// +/// let update = AuthoredUpdate::new("harvest").unwrap(); +/// let _ = GenericEventDraft::from_authored_update(&update, 1, "00"); +/// ``` +#[cfg_attr(any(feature = "serde", test), derive(serde::Serialize))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GenericEventDraft { + contract: ContractKey, + kind: EventKind, + created_at: EventTimestamp, + tags: EventTags, + content: String, + expected_pubkey: PublicKey, + expected_event_id: EventId, +} + +impl GenericEventDraft { + pub fn new( + contract_id: impl Into<String>, + kind: u32, + created_at: u64, + tags: Vec<Vec<String>>, + content: impl Into<String>, + expected_pubkey: impl AsRef<str>, + ) -> Result<Self, DraftError> { + let raw_contract_id = contract_id.into(); + let contract = ContractKey::current(raw_contract_id.clone()) + .map_err(|_| DraftError::UnknownContract(raw_contract_id))?; + let definition = contract.contract(); + crate::require_invariant(definition.kind == kind, &|| { + DraftError::ContractKindMismatch { + contract_id: definition.id.to_owned(), + expected_kind: definition.kind, + actual_kind: kind, + } + })?; + crate::require_invariant( + definition.authoring_policy() == EventAuthoringPolicy::GenericDraft, + &|| DraftError::ContractNotDraftAuthorable { + contract_id: definition.id.to_owned(), + }, + )?; + let content = content.into(); + validate_event_contract_parts(kind, &tags, &content, definition.id).map_err(|error| { + DraftError::ContractShape { + contract_id: definition.id.to_owned(), + error, + } + })?; + let tags = EventTags::new(tags)?; + let expected_pubkey = parse_public_key(expected_pubkey.as_ref())?; + let expected_event_id = compute_canonical_nip01_event_id( + &expected_pubkey.to_hex(), + created_at, + kind, + &tags.to_vec(), + &content, + )?; + Ok(Self { + contract, + kind: EventKind::new(kind), + created_at: EventTimestamp::new(created_at), + tags, + content, + expected_pubkey, + expected_event_id, + }) + } + + pub fn validate_for_authoring(&self) -> Result<(), DraftError> { + let reconstructed = Self::new( + self.contract.contract_id().as_str(), + self.kind_u32(), + self.created_at_u64(), + self.tags_as_vec(), + self.content.clone(), + self.expected_pubkey.to_hex(), + )?; + crate::require_invariant(reconstructed.contract == self.contract, &|| { + DraftError::ContractRegistryVersionMismatch { + expected: RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, + actual: self.contract.registry_version().get(), + } + })?; + crate::require_invariant( + reconstructed.expected_event_id == self.expected_event_id, + &|| DraftError::DraftExpectedEventIdMismatch { + expected_event_id: reconstructed.expected_event_id.to_hex(), + actual_event_id: self.expected_event_id.to_hex(), + }, + ) + } + + pub fn nip01_preimage(&self) -> Result<String, DraftError> { + Ok(canonical_nip01_event_id_preimage( + &self.expected_pubkey.to_hex(), + self.created_at_u64(), + self.kind_u32(), + &self.tags_as_vec(), + self.content(), + )?) + } + + #[must_use] + pub const fn contract(&self) -> &ContractKey { + &self.contract + } + + #[must_use] + pub fn contract_id(&self) -> &str { + self.contract.contract_id().as_str() + } + + #[must_use] + pub const fn kind(&self) -> EventKind { + self.kind + } + + #[must_use] + pub const fn kind_u32(&self) -> u32 { + self.kind.as_u32() + } + + #[must_use] + pub const fn created_at(&self) -> EventTimestamp { + self.created_at + } + + #[must_use] + pub const fn created_at_u64(&self) -> u64 { + self.created_at.as_u64() + } + + #[must_use] + pub const fn tags(&self) -> &EventTags { + &self.tags + } + + #[must_use] + pub fn tags_as_vec(&self) -> Vec<Vec<String>> { + self.tags.to_vec() + } + + #[must_use] + pub fn content(&self) -> &str { + &self.content + } + + #[must_use] + pub const fn expected_pubkey(&self) -> &PublicKey { + &self.expected_pubkey + } + + #[must_use] + pub const fn expected_event_id(&self) -> &EventId { + &self.expected_event_id + } + + #[must_use] + pub fn expected_event_id_hex(&self) -> String { + self.expected_event_id.to_hex() + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for GenericEventDraft { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct GenericDraftSerde { + contract: ContractKey, + kind: EventKind, + created_at: EventTimestamp, + tags: EventTags, + content: String, + expected_pubkey: PublicKey, + expected_event_id: EventId, + } + + let value = GenericDraftSerde::deserialize(deserializer)?; + let draft = Self::new( + value.contract.contract_id().as_str(), + value.kind.as_u32(), + value.created_at.as_u64(), + value.tags.to_vec(), + value.content, + value.expected_pubkey.to_hex(), + ) + .map_err(serde::de::Error::custom)?; + if draft.contract != value.contract { + return Err(serde::de::Error::custom( + "generic draft contract key mismatch", + )); + } + if draft.expected_event_id != value.expected_event_id { + return Err(serde::de::Error::custom( + DraftError::DraftExpectedEventIdMismatch { + expected_event_id: draft.expected_event_id.to_hex(), + actual_event_id: value.expected_event_id.to_hex(), + }, + )); + } + Ok(draft) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + contract::{EventAuthoringPolicy, RegistryVersion, all_event_contracts}, + envelope::kind::KIND_GEOCHAT, + }; + + const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn draft() -> GenericEventDraft { + GenericEventDraft::new( + "radroots.social.geochat.v1", + KIND_GEOCHAT, + 1_700_000_000, + Vec::new(), + "hello", + PUBLIC_KEY, + ) + .expect("generic draft") + } + + #[test] + fn generic_input_binds_exact_contract_author_timestamp_and_wire() { + let draft = draft(); + assert_eq!(draft.contract_id(), "radroots.social.geochat.v1"); + assert_eq!( + draft.contract().registry_version(), + RegistryVersion::CURRENT + ); + assert_eq!(draft.kind_u32(), KIND_GEOCHAT); + assert_eq!(draft.created_at_u64(), 1_700_000_000); + assert_eq!(draft.expected_pubkey().to_hex(), PUBLIC_KEY); + assert_eq!( + draft.nip01_preimage().expect("preimage"), + "[0,\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",1700000000,20000,[],\"hello\"]" + ); + draft.validate_for_authoring().expect("valid draft"); + } + + #[test] + fn generic_input_rejects_every_non_generic_registry_contract() { + for contract in all_event_contracts() + .iter() + .filter(|contract| contract.authoring_policy() != EventAuthoringPolicy::GenericDraft) + { + let error = GenericEventDraft::new( + contract.id, + contract.kind, + 1, + Vec::new(), + "hello", + PUBLIC_KEY, + ) + .expect_err("typed-only and read-only contracts must fail before shape validation"); + assert_eq!( + error, + DraftError::ContractNotDraftAuthorable { + contract_id: contract.id.to_owned(), + }, + "{}", + contract.id + ); + } + } + + #[test] + fn serde_reconstruction_revalidates_all_private_fields() { + let draft = draft(); + let value = serde_json::to_value(&draft).expect("draft JSON"); + assert_eq!( + serde_json::from_value::<GenericEventDraft>(value.clone()) + .expect("decoded generic draft"), + draft + ); + + for (field, replacement) in [ + ("kind", serde_json::json!(1)), + ("content", serde_json::json!("")), + ("expected_event_id", serde_json::json!("f".repeat(64))), + ] { + let mut invalid = value.clone(); + invalid[field] = replacement; + assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err()); + } + let mut invalid = value.clone(); + invalid["contract"]["contract_id"] = serde_json::json!("radroots.social.update.v1"); + assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err()); + let mut invalid = value; + invalid["unexpected"] = serde_json::json!(true); + assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err()); + } +} diff --git a/crates/event/src/lib.rs b/crates/event/src/lib.rs @@ -6,9 +6,7 @@ //! Canonical Radroots event-domain models. //! //! Public behavior is grouped under the approved singular domain modules. -//! The crate root intentionally exposes only [`Event`], [`EventDraft`], -//! [`SignedEvent`], [`VerifiedEvent`], [`EventId`], [`EventKind`], [`EventTag`], -//! and [`Error`]. +//! The crate root intentionally exposes only its curated native event types. #[cfg(not(feature = "std"))] extern crate alloc; @@ -53,6 +51,7 @@ mod dto; pub mod envelope; pub mod farm; pub mod food; +pub mod generic_draft; pub mod id; #[cfg(feature = "knowledge")] pub mod knowledge; @@ -68,5 +67,6 @@ pub mod wire; pub use draft::{EventDraft, SignedEvent}; pub use envelope::{EventEnvelope as Event, EventKind, EventTag}; +pub use generic_draft::GenericEventDraft; pub use id::EventId; pub use verification::{Error, SignatureVerifiedEvent as VerifiedEvent}; diff --git a/crates/event/tests/package_boundary.rs b/crates/event/tests/package_boundary.rs @@ -120,6 +120,7 @@ fn crate_root_declares_every_approved_module() { "envelope", "farm", "food", + "generic_draft", "id", "knowledge", "listing", @@ -143,6 +144,7 @@ fn crate_root_exposes_the_curated_native_event_vocabulary() { type CuratedRootTypes = ( radroots_event::Event, radroots_event::EventDraft, + radroots_event::GenericEventDraft, radroots_event::SignedEvent, radroots_event::VerifiedEvent, radroots_event::EventId, @@ -154,6 +156,7 @@ fn crate_root_exposes_the_curated_native_event_vocabulary() { for root_export in [ "Event", "EventDraft", + "GenericEventDraft", "SignedEvent", "VerifiedEvent", "EventId", diff --git a/crates/nostr/tests/authored_wire_corpus.rs b/crates/nostr/tests/authored_wire_corpus.rs @@ -8,7 +8,7 @@ use nostr::{ }; use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256}; use radroots_event::{ - draft::EventDraft, + GenericEventDraft, envelope::kind::KIND_CLASSIFIED_LISTING, food::availability::{ FoodAvailabilityDetails, FoodAvailabilityDetailsParts, FoodAvailabilityStatus, FoodContent, @@ -325,7 +325,7 @@ fn generic_vector( content: String, keys: &Keys, ) -> WireVector { - let draft = EventDraft::new( + let draft = GenericEventDraft::new( contract_id, kind, created_at, @@ -335,8 +335,8 @@ fn generic_vector( ) .expect("validated generic draft"); draft - .validate_for_signing() - .expect("signable generic draft"); + .validate_for_authoring() + .expect("authorable generic draft"); let nostr_tags = draft .tags_as_vec() .into_iter()