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:
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()