commit 4a6f69d1d35b4e909423b7c47a90eb9fdf9b7699
parent f0462b2728c017b44136caacf62916fe30732592
Author: triesap <tyson@radroots.org>
Date: Fri, 2 Oct 2026 12:58:35 +0000
availability: define admitted availability and unsupported-head views
- Bind immutable version views to shared verification and exact bounded wire
- Preserve tolerant values, unsupported raw heads and shared deletion evidence
- Keep optional display metadata and observation provenance safely bounded
- Verify eighteen frozen cases and current Rust, native and source-lock guards
Diffstat:
8 files changed, 1675 insertions(+), 4 deletions(-)
diff --git a/core/Cargo.lock b/core/Cargo.lock
@@ -992,6 +992,7 @@ version = "0.1.0-alpha"
dependencies = [
"bech32",
"radroots_event",
+ "radroots_event_codec",
"radroots_identity",
"secrecy",
"zeroize",
diff --git a/core/crates/harvestcircle_domain/Cargo.toml b/core/crates/harvestcircle_domain/Cargo.toml
@@ -14,6 +14,7 @@ include = ["src/**", "Cargo.toml"]
[dependencies]
bech32 = "=0.11.1"
radroots_event.workspace = true
+radroots_event_codec = { workspace = true, features = ["json"] }
radroots_identity.workspace = true
secrecy = "=0.10.3"
zeroize = "=1.9.0"
diff --git a/core/crates/harvestcircle_domain/src/availability/mod.rs b/core/crates/harvestcircle_domain/src/availability/mod.rs
@@ -1,5 +1,10 @@
//! Public availability references kept separate from local account authority.
pub mod identity;
+pub mod projection;
pub use identity::{AvailabilityEventVersion, AvailabilityListingCoordinate, PublicPublisher};
+pub use projection::{
+ AvailabilityHeadState, AvailabilityHeadView, AvailabilityObservation,
+ AvailabilityUnsupportedReason, AvailabilityVersionView,
+};
diff --git a/core/crates/harvestcircle_domain/src/availability/projection.rs b/core/crates/harvestcircle_domain/src/availability/projection.rs
@@ -0,0 +1,428 @@
+//! Immutable public availability evidence and retained head views.
+//!
+//! Shared verification, tolerant admission and deletion evaluation own protocol
+//! meaning. These views establish no physical stock, publisher ownership or
+//! network authority. Original JSON extras remain unauthenticated observations.
+
+use std::fmt;
+
+use radroots_event::envelope::EventTimestamp;
+use radroots_event::envelope::event_head::{
+ CurrentEventHead, EventHeadCandidateResult, EventHeadCoordinate,
+ event_head_candidate_for_nip01_event,
+};
+use radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING;
+use radroots_event::id::{RADROOTS_NIP01_COORDINATE_MAX_BYTES, RelayUrl};
+use radroots_event::listing::classified::ClassifiedListingPartition;
+use radroots_event::wire::{DEFAULT_RAW_JSON_MAX_BYTES, Nip01EventWire};
+use radroots_event_codec::admission::deletion::{
+ RadrootsAdmittedNip09DeletionRequestEvent, RadrootsNip09SuppressionDecision,
+ RadrootsNip09SuppressionOutcome, evaluate_nip09_suppression_from_borrowed_requests_v1,
+};
+use radroots_event_codec::admission::food_availability::{
+ RadrootsFoodAvailabilityAdmissionError, RadrootsFoodAvailabilityAdmissionOutcome,
+ admit_verified_food_availability_event,
+};
+use radroots_event_codec::decode::food_availability::RadrootsInboundFoodAvailabilityProjection;
+use radroots_event_codec::verify::RadrootsSignatureVerifiedEvent;
+
+use crate::{
+ Kind0ProfileCandidate, ProfileMetadata, PublicKey, SafeError, SafeErrorCode, SafeMessage,
+ UnixTimestamp,
+};
+
+use super::{AvailabilityEventVersion, AvailabilityListingCoordinate, PublicPublisher};
+
+const MAX_OBSERVATION_SOURCE_BYTES: usize = 2048;
+const MAX_DELETION_REQUESTS: usize = 4096;
+const LISTING_COORDINATE_FRAMING_BYTES: usize = "30402:".len() + 64 + 1;
+
+/// A bounded, exact source reference and local observation time.
+///
+/// The source records provenance only. Pure shared URL validation neither
+/// normalizes endpoint aliases nor authorizes a destination or connection.
+#[derive(Clone, Eq, PartialEq)]
+pub struct AvailabilityObservation {
+ source: RelayUrl,
+ observed_at: UnixTimestamp,
+}
+
+impl AvailabilityObservation {
+ /// Parses a pure source reference after checking its UTF-8 byte bound.
+ ///
+ /// # Errors
+ ///
+ /// Returns a static safe error for an oversized or invalid source reference.
+ pub fn parse(source: &str, observed_at: UnixTimestamp) -> Result<Self, SafeError> {
+ if source.len() > MAX_OBSERVATION_SOURCE_BYTES {
+ return Err(invalid_view());
+ }
+ let source = RelayUrl::parse(source).map_err(|_| invalid_view())?;
+ Ok(Self {
+ source,
+ observed_at,
+ })
+ }
+
+ #[must_use]
+ pub const fn source(&self) -> &RelayUrl {
+ &self.source
+ }
+
+ #[must_use]
+ pub const fn observed_at(&self) -> UnixTimestamp {
+ self.observed_at
+ }
+}
+
+impl fmt::Debug for AvailabilityObservation {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("AvailabilityObservation")
+ .field("source_bytes", &self.source.as_str().len())
+ .field("observed_at", &self.observed_at)
+ .finish()
+ }
+}
+
+/// The shared reason a verified listing cannot provide focused Food data.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum AvailabilityUnsupportedReason {
+ Excluded(ClassifiedListingPartition),
+ ProjectionRejected(&'static str),
+}
+
+#[derive(Clone, Eq, PartialEq)]
+enum AvailabilityProjection {
+ Focused(Box<RadrootsInboundFoodAvailabilityProjection>),
+ Unsupported(AvailabilityUnsupportedReason),
+}
+
+/// One exact verified version, its original wire and tolerant admission result.
+///
+/// The raw head coordinate survives independently of the narrower app reference.
+/// Profile metadata, when present, is caller-supplied author-associated display
+/// data; it carries no signature typestate or farm-ownership proof.
+#[derive(Clone, Eq, PartialEq)]
+pub struct AvailabilityVersionView {
+ verified_event: RadrootsSignatureVerifiedEvent,
+ original_json: String,
+ raw_coordinate: EventHeadCoordinate,
+ listing_coordinate: Option<AvailabilityListingCoordinate>,
+ projection: AvailabilityProjection,
+ observation: AvailabilityObservation,
+ profile_metadata: Option<ProfileMetadata>,
+}
+
+impl AvailabilityVersionView {
+ /// Binds shared signature evidence to the complete original wire envelope.
+ ///
+ /// Original formatting and unknown JSON extras are retained exactly. Extras
+ /// are outside the signed envelope and do not gain authenticated meaning.
+ /// Shared tolerant admission preserves valid exclusions and projection
+ /// rejections as unsupported evidence of this version.
+ ///
+ /// # Errors
+ ///
+ /// Returns a static safe error for wire limits, structural errors, a wire
+ /// mismatch, an unsupported kind or an unavailable shared admission result.
+ pub fn from_verified(
+ verified_event: RadrootsSignatureVerifiedEvent,
+ original_json: &str,
+ observation: AvailabilityObservation,
+ ) -> Result<Self, SafeError> {
+ if original_json.len() > DEFAULT_RAW_JSON_MAX_BYTES
+ || verified_event.event().kind_u32() != KIND_CLASSIFIED_LISTING
+ {
+ return Err(invalid_view());
+ }
+ let envelope = Nip01EventWire::parse_json_unverified(original_json)
+ .map_err(|_| invalid_view())?
+ .into_unverified_envelope()
+ .map_err(|_| invalid_view())?;
+ if &envelope != verified_event.event() {
+ return Err(invalid_view());
+ }
+ let raw_coordinate = match event_head_candidate_for_nip01_event(verified_event.event()) {
+ EventHeadCandidateResult::Candidate(candidate) => candidate.coordinate,
+ _ => return Err(invalid_view()),
+ };
+ let listing_coordinate = app_listing_coordinate(&raw_coordinate);
+ // Full default-limit wire equality precedes this bounded evidence copy.
+ let projection = match admit_verified_food_availability_event(verified_event.clone()) {
+ Ok(RadrootsFoodAvailabilityAdmissionOutcome::Admitted(admitted)) => {
+ let (_, projection) = (*admitted).into_parts();
+ AvailabilityProjection::Focused(Box::new(projection))
+ }
+ Ok(RadrootsFoodAvailabilityAdmissionOutcome::Excluded(excluded)) => {
+ AvailabilityProjection::Unsupported(AvailabilityUnsupportedReason::Excluded(
+ excluded.partition(),
+ ))
+ }
+ Err(RadrootsFoodAvailabilityAdmissionError::Projection(error)) => {
+ AvailabilityProjection::Unsupported(
+ AvailabilityUnsupportedReason::ProjectionRejected(error.code()),
+ )
+ }
+ _ => return Err(invalid_view()),
+ };
+ Ok(Self {
+ verified_event,
+ original_json: original_json.to_owned(),
+ raw_coordinate,
+ listing_coordinate,
+ projection,
+ observation,
+ profile_metadata: None,
+ })
+ }
+
+ #[must_use]
+ pub fn version(&self) -> AvailabilityEventVersion {
+ AvailabilityEventVersion::from_canonical(*self.verified_event.event().id())
+ }
+
+ #[must_use]
+ pub fn publisher(&self) -> PublicPublisher {
+ PublicPublisher::from_public_key(PublicKey::from_canonical(
+ *self.verified_event.event().author(),
+ ))
+ }
+
+ #[must_use]
+ pub fn created_at(&self) -> EventTimestamp {
+ self.verified_event.event().created_at()
+ }
+
+ #[must_use]
+ pub fn original_json(&self) -> &str {
+ &self.original_json
+ }
+
+ #[must_use]
+ pub const fn observation(&self) -> &AvailabilityObservation {
+ &self.observation
+ }
+
+ #[must_use]
+ pub const fn raw_coordinate(&self) -> &EventHeadCoordinate {
+ &self.raw_coordinate
+ }
+
+ #[must_use]
+ pub fn listing_coordinate(&self) -> Option<&AvailabilityListingCoordinate> {
+ self.listing_coordinate.as_ref()
+ }
+
+ #[must_use]
+ pub fn focused(&self) -> Option<&RadrootsInboundFoodAvailabilityProjection> {
+ match &self.projection {
+ AvailabilityProjection::Focused(projection) => Some(projection),
+ AvailabilityProjection::Unsupported(_) => None,
+ }
+ }
+
+ #[must_use]
+ pub const fn unsupported_reason(&self) -> Option<&AvailabilityUnsupportedReason> {
+ match &self.projection {
+ AvailabilityProjection::Focused(_) => None,
+ AvailabilityProjection::Unsupported(reason) => Some(reason),
+ }
+ }
+
+ #[must_use]
+ pub fn profile_metadata(&self) -> Option<&ProfileMetadata> {
+ self.profile_metadata.as_ref()
+ }
+
+ /// Returns a copy with optional caller-supplied, author-associated metadata.
+ ///
+ /// Matching authors do not establish a verified profile or ownership.
+ /// Rejection leaves this immutable listing view usable.
+ ///
+ /// # Errors
+ ///
+ /// Returns a static safe error when the supplied candidate author differs.
+ pub fn with_profile(&self, profile: Option<&Kind0ProfileCandidate>) -> Result<Self, SafeError> {
+ if profile.is_some_and(|candidate| candidate.author() != self.publisher().public_key()) {
+ return Err(invalid_view());
+ }
+ let mut view = self.clone();
+ view.profile_metadata = profile.map(|candidate| candidate.metadata().clone());
+ Ok(view)
+ }
+}
+
+impl fmt::Debug for AvailabilityVersionView {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("AvailabilityVersionView")
+ .field("created_at", &self.created_at())
+ .field("wire_bytes", &self.original_json.len())
+ .field("focused", &self.focused().is_some())
+ .field("unsupported_reason", &self.unsupported_reason())
+ .field("profile_present", &self.profile_metadata.is_some())
+ .field("observation", &self.observation)
+ .finish()
+ }
+}
+
+/// The retained selected head's state, without a physical-supply claim.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum AvailabilityHeadState {
+ Missing,
+ Focused,
+ Unsupported,
+ Deleted,
+}
+
+#[derive(Clone, Eq, PartialEq)]
+enum AvailabilityHeadEvidence {
+ Missing(AvailabilityListingCoordinate),
+ Selected {
+ version: Box<AvailabilityVersionView>,
+ suppression: RadrootsNip09SuppressionDecision,
+ },
+}
+
+/// Absence of retained knowledge or an exact selected version and shared decision.
+///
+/// Deleted versions remain available as historical evidence. Neither missing
+/// knowledge nor listing query completion establishes exhaustive deletion data.
+#[derive(Clone, Eq, PartialEq)]
+pub struct AvailabilityHeadView {
+ evidence: AvailabilityHeadEvidence,
+}
+
+impl AvailabilityHeadView {
+ #[must_use]
+ pub const fn missing(coordinate: AvailabilityListingCoordinate) -> Self {
+ Self {
+ evidence: AvailabilityHeadEvidence::Missing(coordinate),
+ }
+ }
+
+ /// Validates exact correspondence to the supplied shared selected head and
+ /// delegates suppression to the shared evaluator over borrowed requests.
+ ///
+ /// # Errors
+ ///
+ /// Returns a static safe error when coordinate, event ID or signed time
+ /// differs, or more than4096 admitted deletion requests are supplied.
+ pub fn from_selected(
+ selected: CurrentEventHead,
+ version: AvailabilityVersionView,
+ requests: &[RadrootsAdmittedNip09DeletionRequestEvent],
+ ) -> Result<Self, SafeError> {
+ if requests.len() > MAX_DELETION_REQUESTS
+ || &selected.coordinate != version.raw_coordinate()
+ || selected.event_id != version.version().event_id()
+ || selected.created_at != version.created_at().as_u64()
+ {
+ return Err(invalid_view());
+ }
+ let suppression = evaluate_nip09_suppression_from_borrowed_requests_v1(
+ &version.verified_event,
+ requests.iter(),
+ );
+ Ok(Self {
+ evidence: AvailabilityHeadEvidence::Selected {
+ version: Box::new(version),
+ suppression,
+ },
+ })
+ }
+
+ #[must_use]
+ pub fn state(&self) -> AvailabilityHeadState {
+ match &self.evidence {
+ AvailabilityHeadEvidence::Missing(_) => AvailabilityHeadState::Missing,
+ AvailabilityHeadEvidence::Selected {
+ version,
+ suppression,
+ } => {
+ if suppression.outcome() == RadrootsNip09SuppressionOutcome::Suppressed {
+ AvailabilityHeadState::Deleted
+ } else if version.focused().is_some() {
+ AvailabilityHeadState::Focused
+ } else {
+ AvailabilityHeadState::Unsupported
+ }
+ }
+ }
+ }
+
+ /// Returns the retained version, including historical deleted evidence.
+ #[must_use]
+ pub fn version(&self) -> Option<&AvailabilityVersionView> {
+ match &self.evidence {
+ AvailabilityHeadEvidence::Missing(_) => None,
+ AvailabilityHeadEvidence::Selected { version, .. } => Some(version),
+ }
+ }
+
+ #[must_use]
+ pub const fn suppression(&self) -> Option<&RadrootsNip09SuppressionDecision> {
+ match &self.evidence {
+ AvailabilityHeadEvidence::Missing(_) => None,
+ AvailabilityHeadEvidence::Selected { suppression, .. } => Some(suppression),
+ }
+ }
+
+ /// Returns focused data only when the selected version remains visible.
+ #[must_use]
+ pub fn focused(&self) -> Option<&RadrootsInboundFoodAvailabilityProjection> {
+ if self.state() == AvailabilityHeadState::Focused {
+ self.version().and_then(AvailabilityVersionView::focused)
+ } else {
+ None
+ }
+ }
+}
+
+impl fmt::Debug for AvailabilityHeadView {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut summary = formatter.debug_struct("AvailabilityHeadView");
+ summary.field("state", &self.state());
+ match &self.evidence {
+ AvailabilityHeadEvidence::Missing(coordinate) => {
+ summary.field("coordinate_bytes", &coordinate.as_str().len());
+ }
+ AvailabilityHeadEvidence::Selected {
+ version,
+ suppression,
+ } => {
+ summary
+ .field("version", version)
+ .field("suppression_reason", &suppression.reason());
+ }
+ }
+ summary.finish()
+ }
+}
+
+fn app_listing_coordinate(raw: &EventHeadCoordinate) -> Option<AvailabilityListingCoordinate> {
+ let EventHeadCoordinate::Addressable {
+ kind,
+ pubkey,
+ d_tag,
+ } = raw
+ else {
+ return None;
+ };
+ if *kind != KIND_CLASSIFIED_LISTING
+ || d_tag.is_empty()
+ || d_tag.len() > RADROOTS_NIP01_COORDINATE_MAX_BYTES - LISTING_COORDINATE_FRAMING_BYTES
+ {
+ return None;
+ }
+ // Borrowed framing bounds precede author encoding and coordinate allocation.
+ AvailabilityListingCoordinate::parse(&format!("30402:{}:{d_tag}", pubkey.to_hex())).ok()
+}
+
+const fn invalid_view() -> SafeError {
+ SafeError::new(
+ SafeErrorCode::InvalidProfileMetadata,
+ SafeMessage::new("The public availability view is invalid."),
+ )
+}
diff --git a/core/crates/harvestcircle_domain/src/lib.rs b/core/crates/harvestcircle_domain/src/lib.rs
@@ -7,7 +7,11 @@ pub mod key;
pub mod profile;
pub mod time;
-pub use availability::{AvailabilityEventVersion, AvailabilityListingCoordinate, PublicPublisher};
+pub use availability::{
+ AvailabilityEventVersion, AvailabilityHeadState, AvailabilityHeadView,
+ AvailabilityListingCoordinate, AvailabilityObservation, AvailabilityUnsupportedReason,
+ AvailabilityVersionView, PublicPublisher,
+};
pub use error::{SafeError, SafeErrorCode, SafeMessage};
pub use identity::{
IdentityCreatedAt, IdentityLabel, LocalKeyringBinding, NostrIdentity, NostrIdentityReference,
diff --git a/core/crates/harvestcircle_nostr/tests/focused_availability_projection.rs b/core/crates/harvestcircle_nostr/tests/focused_availability_projection.rs
@@ -0,0 +1,1232 @@
+use harvestcircle_domain::{
+ AvailabilityEventVersion, AvailabilityHeadState, AvailabilityHeadView,
+ AvailabilityListingCoordinate, AvailabilityObservation, AvailabilityUnsupportedReason,
+ AvailabilityVersionView, EventId, Kind0ProfileCandidate, ProfileMetadata, PublicKey, SafeError,
+ SafeErrorCode, UnixTimestamp,
+};
+use harvestcircle_nostr::parse_verified_kind0;
+use nostr::{Event, EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp};
+use radroots_event::envelope::event_head::{
+ CurrentEventHead, EventHeadCandidate, EventHeadCandidateResult, EventHeadCoordinate,
+ EventHeadDecision, event_head_candidate_for_nip01_event, select_event_head,
+};
+use radroots_event::listing::classified::ClassifiedListingPartition;
+use radroots_event::wire::{
+ DEFAULT_EXTRA_MAX_FIELDS, DEFAULT_EXTRA_TOTAL_JSON_MAX_BYTES, DEFAULT_RAW_JSON_MAX_BYTES,
+ DEFAULT_TAG_ELEMENT_MAX_BYTES, Nip01EventWire,
+};
+use radroots_event_codec::admission::deletion::{
+ RadrootsAdmittedNip09DeletionRequestEvent, RadrootsNip09SuppressionOutcome,
+ RadrootsNip09SuppressionReason, evaluate_nip09_suppression_from_borrowed_requests_v1,
+ verify_and_admit_nip09_deletion_request_event,
+};
+use radroots_event_codec::admission::food_availability::{
+ RadrootsFoodAvailabilityAdmissionOutcome, admit_verified_food_availability_event,
+};
+use radroots_event_codec::decode::food_availability::RadrootsFoodAvailabilityImageDiagnostic;
+use radroots_event_codec::verify::{
+ RadrootsNip01VerificationError, RadrootsSignatureVerifiedEvent, verify_nip01_event,
+};
+
+#[path = "fixtures/availability_admission_v1.rs"]
+mod admission;
+#[path = "fixtures/availability_lifecycle_v1.rs"]
+mod lifecycle;
+
+const MAX_RECIPE_BYTES: usize = 8 * 1024;
+const MAX_RECIPE_TAGS: usize = 32;
+const MAX_RECIPE_ELEMENTS: usize = 128;
+const MAX_SIGNED_EVENT_BYTES: usize = 16 * 1024;
+const MAX_LIFECYCLE_EVENT_BYTES: usize = 4 * 1024;
+const MAX_LIFECYCLE_CORPUS_BYTES: usize = 64 * 1024;
+const MAX_RETAINED_RECIPE_SOURCE_BYTES: usize = 32 * 1024;
+const MAX_SOURCE_BYTES: usize = 2048;
+const MAX_DELETION_REQUESTS: usize = 4096;
+const OBSERVED_AT: i64 = 1_800_000_500;
+const SOURCE: &str = "wss://relay.example.test/observations?scope=public";
+
+struct PublicFixture {
+ json: String,
+ author: String,
+}
+
+fn assert_recipe_source_bounds() {
+ let sources = [
+ include_str!("fixtures/availability_admission_v1.rs"),
+ include_str!("fixtures/availability_lifecycle_v1.rs"),
+ ];
+ assert!(
+ sources.iter().map(|source| source.len()).sum::<usize>()
+ <= MAX_RETAINED_RECIPE_SOURCE_BYTES
+ );
+ for source in sources {
+ for forbidden in ["nsec1", "secret_key", "private_key", "credential"] {
+ assert!(
+ !source.contains(forbidden),
+ "retained recipes contain only public data"
+ );
+ }
+ }
+}
+
+fn sign_bounded_event(
+ keys: &Keys,
+ kind: u16,
+ created_at: u64,
+ content: &str,
+ tags: Vec<Vec<String>>,
+) -> Event {
+ assert_recipe_source_bounds();
+ assert!(tags.len() <= MAX_RECIPE_TAGS);
+ assert!(tags.iter().map(Vec::len).sum::<usize>() <= MAX_RECIPE_ELEMENTS);
+ assert!(
+ content.len() + tags.iter().flatten().map(String::len).sum::<usize>() <= MAX_RECIPE_BYTES
+ );
+ assert!(
+ tags.iter()
+ .flatten()
+ .all(|value| value.len() <= DEFAULT_TAG_ELEMENT_MAX_BYTES)
+ );
+ EventBuilder::new(Kind::from(kind), content)
+ .tags(
+ tags.into_iter()
+ .map(|tag| Tag::parse(tag).expect("bounded public raw tag")),
+ )
+ .custom_created_at(Timestamp::from(created_at))
+ .sign_with_keys(keys)
+ .expect("isolated public fixture signing")
+}
+
+/// The ephemeral signer is discarded before retained public JSON is returned.
+fn public_fixture_at(recipe: admission::Recipe, kind: u16, created_at: u64) -> PublicFixture {
+ let keys = Keys::generate();
+ let author = keys.public_key().to_hex();
+ let event = sign_bounded_event(&keys, kind, created_at, &recipe.content, recipe.tags);
+ drop(keys);
+ let json = event.as_json();
+ assert!(json.len() <= MAX_SIGNED_EVENT_BYTES);
+ PublicFixture { json, author }
+}
+
+fn public_fixture(recipe: admission::Recipe) -> PublicFixture {
+ public_fixture_at(recipe, 30402, admission::CREATED_AT)
+}
+
+fn verified(json: &str) -> RadrootsSignatureVerifiedEvent {
+ assert!(json.len() <= MAX_SIGNED_EVENT_BYTES);
+ let wire = Nip01EventWire::parse_json_unverified(json).expect("default-bounded public wire");
+ verify_nip01_event(
+ wire.into_unverified_envelope()
+ .expect("structurally valid envelope"),
+ )
+ .expect("real shared ID and signature verification")
+}
+
+fn observation() -> AvailabilityObservation {
+ AvailabilityObservation::parse(
+ SOURCE,
+ UnixTimestamp::from_seconds(OBSERVED_AT).expect("local observation time"),
+ )
+ .expect("bounded pure source reference")
+}
+
+fn version_view(json: &str) -> AvailabilityVersionView {
+ AvailabilityVersionView::from_verified(verified(json), json, observation())
+ .expect("verified version view")
+}
+
+fn candidate(event: &RadrootsSignatureVerifiedEvent) -> EventHeadCandidate {
+ match event_head_candidate_for_nip01_event(event.event()) {
+ EventHeadCandidateResult::Candidate(candidate) => candidate,
+ _ => panic!("verified addressable recipe must retain a raw head"),
+ }
+}
+
+fn selected(events: &[&RadrootsSignatureVerifiedEvent]) -> CurrentEventHead {
+ assert!(!events.is_empty() && events.len() <= 3);
+ let mut current = None;
+ for event in events {
+ match select_event_head(candidate(event), current.as_ref()) {
+ EventHeadDecision::Applied(next) => current = Some(next),
+ EventHeadDecision::SkippedDuplicate
+ | EventHeadDecision::SkippedOlder
+ | EventHeadDecision::SkippedSameTimestampHigherEventId => {}
+ EventHeadDecision::CoordinateMismatch => {
+ panic!("one raw coordinate per ordering fixture")
+ }
+ }
+ }
+ current.expect("shared selected head")
+}
+
+struct LifecycleCorpus {
+ author: String,
+ other_author: String,
+ events: Vec<(&'static str, String)>,
+}
+
+/// Both temporary signers are discarded before the bounded public corpus escapes.
+fn lifecycle_corpus() -> LifecycleCorpus {
+ let owner = Keys::generate();
+ let other = Keys::generate();
+ let author = owner.public_key().to_hex();
+ let other_author = other.public_key().to_hex();
+ assert_ne!(author, other_author);
+ let mut signed = Vec::<(&'static str, Event)>::new();
+ for version in &lifecycle::VERSIONS {
+ signed.push((
+ version.name,
+ sign_bounded_event(
+ if version.other_author { &other } else { &owner },
+ 30402,
+ version.created_at,
+ version.content,
+ lifecycle::listing_tags(version),
+ ),
+ ));
+ }
+ let original = &lifecycle::VERSIONS[0];
+ signed.push((
+ "other_kind",
+ sign_bounded_event(
+ &owner,
+ lifecycle::OTHER_ADDRESSABLE_KIND,
+ original.created_at,
+ original.content,
+ lifecycle::listing_tags(original),
+ ),
+ ));
+ let address = format!("30402:{author}:{}", lifecycle::IDENTIFIER);
+ for deletion in &lifecycle::DELETIONS {
+ let mut tags = Vec::new();
+ if let lifecycle::Target::Event(name) | lifecycle::Target::EventAndAddress(name) =
+ deletion.target
+ {
+ let (_, target) = signed
+ .iter()
+ .find(|(target_name, _)| *target_name == name)
+ .expect("reused lifecycle target exists");
+ tags.push(vec!["e".into(), target.id.to_hex()]);
+ }
+ if matches!(
+ deletion.target,
+ lifecycle::Target::Address | lifecycle::Target::EventAndAddress(_)
+ ) {
+ tags.push(vec!["a".into(), address.clone()]);
+ }
+ tags.push(vec!["k".into(), "30402".into()]);
+ signed.push((
+ deletion.name,
+ sign_bounded_event(
+ if deletion.other_author {
+ &other
+ } else {
+ &owner
+ },
+ 5,
+ deletion.created_at,
+ "Public deletion conformance.",
+ tags,
+ ),
+ ));
+ }
+ drop(owner);
+ drop(other);
+ let events: Vec<_> = signed
+ .into_iter()
+ .map(|(name, event)| {
+ let json = event.as_json();
+ assert!(json.len() <= MAX_LIFECYCLE_EVENT_BYTES);
+ (name, json)
+ })
+ .collect();
+ assert_eq!(events.len(), 18);
+ assert!(events.iter().map(|(_, json)| json.len()).sum::<usize>() <= MAX_LIFECYCLE_CORPUS_BYTES);
+ LifecycleCorpus {
+ author,
+ other_author,
+ events,
+ }
+}
+
+fn lifecycle_json<'a>(corpus: &'a LifecycleCorpus, name: &str) -> &'a str {
+ &corpus
+ .events
+ .iter()
+ .find(|(event_name, _)| *event_name == name)
+ .expect("named unchanged lifecycle recipe")
+ .1
+}
+
+fn deletion(corpus: &LifecycleCorpus, name: &str) -> RadrootsAdmittedNip09DeletionRequestEvent {
+ let wire = Nip01EventWire::parse_json_unverified(lifecycle_json(corpus, name))
+ .expect("default-bounded deletion wire");
+ verify_and_admit_nip09_deletion_request_event(
+ wire.into_unverified_envelope().expect("request envelope"),
+ )
+ .expect("real shared signature verification and deletion admission")
+}
+
+fn replace_once(json: &str, original: &str, replacement: &str) -> String {
+ assert_eq!(
+ json.matches(original).count(),
+ 1,
+ "unique public fixture token"
+ );
+ let replaced = json.replacen(original, replacement, 1);
+ assert!(replaced.len() <= DEFAULT_RAW_JSON_MAX_BYTES + 1);
+ replaced
+}
+
+fn replace_string_field(json: &str, field: &str, original: &str, replacement: &str) -> String {
+ replace_once(
+ json,
+ &format!("\"{field}\":\"{original}\""),
+ &format!("\"{field}\":\"{replacement}\""),
+ )
+}
+
+fn add_extras(json: &str, extras: &str) -> String {
+ assert!(json.ends_with('}'));
+ assert!(json.len() + extras.len() <= DEFAULT_RAW_JSON_MAX_BYTES);
+ format!("{},{}{}", &json[..json.len() - 1], extras, '}')
+}
+
+fn text_at_bytes(unit: &str, size: usize) -> String {
+ unit.repeat(size / unit.len()) + &"x".repeat(size % unit.len())
+}
+
+fn assert_safe(error: SafeError, rejected_input: &str) {
+ assert_eq!(error.code(), SafeErrorCode::InvalidProfileMetadata);
+ for rendered in [error.to_string(), format!("{error:?}")] {
+ assert!(rendered.len() <= 256);
+ assert!(!rendered.contains(rejected_input));
+ }
+}
+
+#[test]
+fn admitted_view_preserves_exact_version_coordinate_and_signed_times() {
+ let fixture = public_fixture(admission::focused_recipe());
+ let event = verified(&fixture.json);
+ let admitted =
+ match admit_verified_food_availability_event(event.clone()).expect("real admission") {
+ RadrootsFoodAvailabilityAdmissionOutcome::Admitted(admitted) => admitted,
+ _ => panic!("focused recipe"),
+ };
+ let view = version_view(&fixture.json);
+ assert_eq!(view.version().event_id(), *event.event().id());
+ assert_eq!(view.publisher().public_key().to_hex(), fixture.author);
+ assert_eq!(view.created_at(), event.event().created_at());
+ assert_eq!(view.created_at().as_u64(), admission::CREATED_AT);
+ assert_eq!(view.observation().observed_at().as_seconds(), OBSERVED_AT);
+ assert_eq!(view.observation().source().as_str(), SOURCE);
+ assert_eq!(view.original_json(), fixture.json);
+ assert_eq!(view.raw_coordinate(), &candidate(&event).coordinate);
+ assert_eq!(
+ view.listing_coordinate()
+ .expect("strict app reference")
+ .identifier(),
+ "hcav-incoming-v1"
+ );
+ assert_eq!(view.focused(), Some(admitted.projection()));
+ assert_eq!(view.unsupported_reason(), None);
+ assert_eq!(view.profile_metadata(), None);
+
+ let wide = public_fixture_at(admission::focused_recipe(), 30402, i64::MAX as u64 + 1);
+ let wide_view = version_view(&wide.json);
+ assert_eq!(wide_view.created_at().as_u64(), i64::MAX as u64 + 1);
+ assert_eq!(
+ wide_view.observation().observed_at().as_seconds(),
+ OBSERVED_AT
+ );
+}
+
+#[test]
+fn admitted_view_preserves_unknown_quantity_and_exact_normalized_price() {
+ let fixture = public_fixture(admission::focused_recipe());
+ let view = version_view(&fixture.json);
+ let food = view.focused().expect("focused tolerant projection");
+ assert_eq!(food.quantity(), None);
+ assert_eq!(food.price().amount(), "3.5");
+ assert_eq!(food.price().currency().as_str(), "CAD");
+ assert_eq!(food.price().unit().as_str(), "lb");
+ assert_eq!(food.status().as_str(), "active");
+ assert_eq!(food.published_at().as_u64(), 1_800_000_000);
+ assert_eq!(food.content().as_str(), admission::CONTENT);
+ assert_eq!(food.title().as_str(), "Incoming availability conformance");
+ assert_eq!(food.summary().as_str(), "Public incoming protocol fixture");
+ assert_eq!(food.location().as_str(), "Protocol test location");
+}
+
+#[test]
+fn admitted_view_preserves_known_quantity_without_unit_conversion() {
+ for (amount, unit, expected) in [("0012.5000", "lb", "12.5"), ("0.1250", "kg", "0.125")] {
+ let mut recipe = admission::focused_recipe();
+ recipe
+ .tags
+ .iter_mut()
+ .find(|tag| tag[0] == "radroots:price_unit")
+ .expect("price unit")[1] = unit.into();
+ recipe
+ .tags
+ .push(vec!["radroots:quantity".into(), amount.into(), unit.into()]);
+ let fixture = public_fixture(recipe);
+ let view = version_view(&fixture.json);
+ let food = view.focused().expect("focused quantity");
+ let quantity = food.quantity().expect("known quantity");
+ assert_eq!(quantity.amount(), expected);
+ assert_eq!(quantity.unit().as_str(), unit);
+ assert_eq!(food.price().unit().as_str(), unit);
+ assert_eq!(food.price().amount(), "3.5");
+ }
+}
+
+#[test]
+fn admitted_view_retains_optional_image_diagnostics_and_original_wire() {
+ let fixture = public_fixture(admission::edited_recipe(&[
+ admission::Edit::Append(&["image", "https://images.example.test/incoming.webp"]),
+ admission::Edit::Append(&["t", "opaque-public-discovery-tag"]),
+ ]));
+ let exact = format!(
+ " \n{}\t",
+ add_extras(
+ &fixture.json,
+ "\"untrusted_extra\":{\"stock\":999,\"claim\":\"not-signed\"}"
+ )
+ );
+ let view =
+ AvailabilityVersionView::from_verified(verified(&fixture.json), &exact, observation())
+ .expect("exact extras and formatting");
+ assert_eq!(view.original_json().as_bytes(), exact.as_bytes());
+ let food = view
+ .focused()
+ .expect("optional image cannot discard listing");
+ assert_eq!(food.quantity(), None);
+ assert_eq!(food.images().len(), 1);
+ let image = &food.images()[0];
+ assert_eq!(
+ image.url(),
+ Some("https://images.example.test/incoming.webp")
+ );
+ assert_eq!(image.dimensions(), None);
+ assert!(!image.qualifies());
+ assert_eq!(
+ image.raw_tag(),
+ &["image", "https://images.example.test/incoming.webp"]
+ );
+ assert!(
+ image
+ .diagnostics()
+ .contains(&RadrootsFoodAvailabilityImageDiagnostic::ShapeInvalid)
+ );
+ assert!(
+ image
+ .diagnostics()
+ .contains(&RadrootsFoodAvailabilityImageDiagnostic::DimensionsMissing)
+ );
+ let retained =
+ Nip01EventWire::parse_json_unverified(view.original_json()).expect("retained wire");
+ assert!(
+ retained
+ .tags
+ .iter()
+ .any(|tag| tag == &["t", "opaque-public-discovery-tag"])
+ );
+ assert_eq!(retained.extra.len(), 1);
+ assert_eq!(
+ retained
+ .into_unverified_envelope()
+ .expect("extras have no envelope authority"),
+ *verified(&fixture.json).event()
+ );
+}
+
+#[test]
+fn profile_failure_does_not_remove_an_admitted_listing_view() {
+ let keys = Keys::generate();
+ let author = keys.public_key().to_hex();
+ let recipe = admission::focused_recipe();
+ let listing = sign_bounded_event(
+ &keys,
+ 30402,
+ admission::CREATED_AT,
+ &recipe.content,
+ recipe.tags,
+ );
+ let malformed_profile =
+ sign_bounded_event(&keys, 0, admission::CREATED_AT, "not profile JSON", vec![]);
+ let overlong_profile = sign_bounded_event(
+ &keys,
+ 0,
+ admission::CREATED_AT,
+ &format!("{{\"name\":\"{}\"}}", "p".repeat(129)),
+ vec![],
+ );
+ drop(keys);
+ let listing_json = listing.as_json();
+ let view = version_view(&listing_json);
+ let publisher = PublicKey::from_hex(&author).expect("public profile author");
+ for event in [malformed_profile, overlong_profile] {
+ let json = event.as_json();
+ assert!(json.len() <= MAX_SIGNED_EVENT_BYTES);
+ assert!(parse_verified_kind0(&json, publisher).is_err());
+ assert_eq!(view.profile_metadata(), None);
+ assert_eq!(
+ view.focused()
+ .expect("listing survives optional metadata failure")
+ .status()
+ .as_str(),
+ "active"
+ );
+ assert_eq!(view.original_json(), listing_json);
+ }
+ assert_eq!(
+ view.with_profile(None)
+ .expect("absence remains absence")
+ .profile_metadata(),
+ None
+ );
+}
+
+#[test]
+fn generic_and_operational_heads_never_become_focused_food() {
+ let generic = public_fixture(admission::recipe_with_tags(&[]));
+ let view = version_view(&generic.json);
+ assert_eq!(view.focused(), None);
+ assert_eq!(
+ view.unsupported_reason(),
+ Some(&AvailabilityUnsupportedReason::Excluded(
+ ClassifiedListingPartition::GenericNip99
+ ))
+ );
+ for marker in admission::OPERATIONAL_MARKERS {
+ let fixture = public_fixture(admission::recipe_with_tags(&[&[marker]]));
+ let view = version_view(&fixture.json);
+ assert_eq!(view.focused(), None);
+ assert_eq!(
+ view.unsupported_reason(),
+ Some(&AvailabilityUnsupportedReason::Excluded(
+ ClassifiedListingPartition::OperationalListing
+ ))
+ );
+ let head =
+ AvailabilityHeadView::from_selected(selected(&[&verified(&fixture.json)]), view, &[])
+ .expect("raw operational head retained");
+ assert_eq!(head.state(), AvailabilityHeadState::Unsupported);
+ assert_eq!(head.focused(), None);
+ }
+ assert_eq!(
+ admission::FOCUSED_MARKERS,
+ ["radroots:price_unit", "radroots:quantity"]
+ );
+}
+
+#[test]
+fn ambiguous_and_malformed_verified_heads_remain_unsupported() {
+ for marker in admission::OPERATIONAL_MARKERS {
+ let mut recipe = admission::focused_recipe();
+ recipe.tags.push(vec![marker.into()]);
+ let fixture = public_fixture(recipe);
+ let view = version_view(&fixture.json);
+ assert_eq!(view.focused(), None);
+ assert_eq!(
+ view.unsupported_reason(),
+ Some(&AvailabilityUnsupportedReason::ProjectionRejected(
+ "food_profile_ambiguous"
+ ))
+ );
+ }
+ for case in &admission::MALFORMED_CASES {
+ let fixture = public_fixture(admission::edited_recipe(case.edits));
+ let event = verified(&fixture.json);
+ assert_eq!(
+ admit_verified_food_availability_event(event.clone()).expect_err(case.name),
+ case.expected
+ );
+ let view = version_view(&fixture.json);
+ assert_eq!(view.version().event_id(), *event.event().id());
+ assert_eq!(view.raw_coordinate(), &candidate(&event).coordinate);
+ assert_eq!(view.focused(), None, "{}", case.name);
+ assert_eq!(
+ view.unsupported_reason(),
+ Some(&AvailabilityUnsupportedReason::ProjectionRejected(
+ case.code
+ )),
+ "{}",
+ case.name
+ );
+ }
+}
+
+#[test]
+fn unsupported_head_preserves_empty_missing_and_long_identifiers() {
+ for identifier in [
+ None,
+ Some(String::new()),
+ Some("d".repeat(4025)),
+ Some("d".repeat(4026)),
+ Some("é".repeat(2048)),
+ Some(" opaque:\0:é ".into()),
+ ] {
+ let mut recipe = admission::focused_recipe();
+ recipe.tags.retain(|tag| tag[0] != "d");
+ if let Some(identifier) = &identifier {
+ recipe.tags.insert(0, vec!["d".into(), identifier.clone()]);
+ }
+ let fixture = public_fixture(recipe);
+ let view = version_view(&fixture.json);
+ assert_eq!(view.focused(), None);
+ assert!(matches!(
+ view.unsupported_reason(),
+ Some(AvailabilityUnsupportedReason::ProjectionRejected(_))
+ ));
+ match view.raw_coordinate() {
+ EventHeadCoordinate::Addressable {
+ kind,
+ pubkey,
+ d_tag,
+ } => {
+ assert_eq!(*kind, 30402);
+ assert_eq!(pubkey.to_hex(), fixture.author);
+ assert_eq!(d_tag, identifier.as_deref().unwrap_or(""));
+ }
+ _ => panic!("raw kind-30402 head"),
+ }
+ let fits_app = identifier
+ .as_ref()
+ .is_some_and(|value| !value.is_empty() && value.len() <= 4025);
+ assert_eq!(view.listing_coordinate().is_some(), fits_app);
+ if let Some(coordinate) = view.listing_coordinate() {
+ assert_eq!(
+ coordinate.identifier(),
+ identifier.as_deref().expect("nonempty app identifier")
+ );
+ }
+ let head =
+ AvailabilityHeadView::from_selected(selected(&[&verified(&fixture.json)]), view, &[])
+ .expect("unsupported raw head survives");
+ assert_eq!(head.state(), AvailabilityHeadState::Unsupported);
+ assert!(head.version().is_some());
+ assert_eq!(head.focused(), None);
+ }
+}
+
+#[test]
+fn newer_unsupported_head_blocks_older_compatible_view() {
+ let corpus = lifecycle_corpus();
+ let old = verified(lifecycle_json(&corpus, "old_active"));
+ assert_eq!(
+ version_view(lifecycle_json(&corpus, "old_active"))
+ .focused()
+ .expect("older food")
+ .status()
+ .as_str(),
+ "active"
+ );
+ for name in ["generic_head", "malformed_head"] {
+ let newer = verified(lifecycle_json(&corpus, name));
+ for order in [[&old, &newer, &old], [&newer, &old, &newer]] {
+ let current = selected(&order);
+ assert_eq!(current.event_id, *newer.event().id());
+ let head = AvailabilityHeadView::from_selected(
+ current.clone(),
+ version_view(lifecycle_json(&corpus, name)),
+ &[],
+ )
+ .expect("selected unsupported version");
+ assert_eq!(head.state(), AvailabilityHeadState::Unsupported);
+ assert_eq!(
+ head.version()
+ .expect("retained winner")
+ .version()
+ .event_id(),
+ *newer.event().id()
+ );
+ assert_eq!(head.focused(), None);
+ assert!(
+ AvailabilityHeadView::from_selected(
+ current,
+ version_view(lifecycle_json(&corpus, "old_active")),
+ &[]
+ )
+ .is_err()
+ );
+ }
+ }
+}
+
+#[test]
+fn missing_unsupported_and_deleted_states_remain_distinct() {
+ let corpus = lifecycle_corpus();
+ let focused = version_view(lifecycle_json(&corpus, "newer_sold"));
+ let missing = AvailabilityHeadView::missing(
+ focused
+ .listing_coordinate()
+ .expect("app coordinate")
+ .clone(),
+ );
+ assert_eq!(missing.state(), AvailabilityHeadState::Missing);
+ assert_eq!(missing.version(), None);
+ assert_eq!(missing.suppression(), None);
+ assert_eq!(missing.focused(), None);
+ let event = verified(lifecycle_json(&corpus, "newer_sold"));
+ let visible = AvailabilityHeadView::from_selected(selected(&[&event]), focused.clone(), &[])
+ .expect("visible selected version");
+ assert_eq!(visible.state(), AvailabilityHeadState::Focused);
+ assert_eq!(
+ visible.focused().expect("visible food").status().as_str(),
+ "sold"
+ );
+ let deleted = AvailabilityHeadView::from_selected(
+ selected(&[&event]),
+ focused,
+ &[deletion(&corpus, "exact_newer")],
+ )
+ .expect("shared suppression");
+ assert_eq!(deleted.state(), AvailabilityHeadState::Deleted);
+ assert_eq!(deleted.focused(), None);
+ assert_eq!(
+ deleted
+ .version()
+ .expect("historical food remains")
+ .focused()
+ .expect("historical projection")
+ .status()
+ .as_str(),
+ "sold"
+ );
+ let generic_json = lifecycle_json(&corpus, "generic_head");
+ let unsupported = AvailabilityHeadView::from_selected(
+ selected(&[&verified(generic_json)]),
+ version_view(generic_json),
+ &[],
+ )
+ .expect("unsupported winner");
+ assert_eq!(unsupported.state(), AvailabilityHeadState::Unsupported);
+ assert_eq!(unsupported.focused(), None);
+}
+
+#[test]
+fn selected_head_mismatch_rejects_an_older_version() {
+ let corpus = lifecycle_corpus();
+ let old_json = lifecycle_json(&corpus, "old_active");
+ let new = verified(lifecycle_json(&corpus, "newer_sold"));
+ assert!(
+ AvailabilityHeadView::from_selected(selected(&[&new]), version_view(old_json), &[])
+ .is_err()
+ );
+ let actual = selected(&[&verified(old_json)]);
+ let mut wrong_id = actual.clone();
+ wrong_id.event_id = *new.event().id();
+ let mut wrong_time = actual.clone();
+ wrong_time.created_at += 1;
+ let mut mismatches = vec![wrong_id, wrong_time];
+ for name in ["other_identifier", "other_author", "other_kind"] {
+ let mut wrong_coordinate = actual.clone();
+ wrong_coordinate.coordinate =
+ candidate(&verified(lifecycle_json(&corpus, name))).coordinate;
+ assert_eq!(wrong_coordinate.event_id, actual.event_id);
+ assert_eq!(wrong_coordinate.created_at, actual.created_at);
+ assert_ne!(wrong_coordinate.coordinate, actual.coordinate);
+ mismatches.push(wrong_coordinate);
+ }
+ assert_eq!(mismatches.len(), 5);
+ for wrong in mismatches {
+ let error = AvailabilityHeadView::from_selected(wrong, version_view(old_json), &[])
+ .expect_err("all selected head fields bind the exact version");
+ assert_safe(error, "Public lifecycle conformance.");
+ }
+ assert!(AvailabilityHeadView::from_selected(actual, version_view(old_json), &[]).is_ok());
+}
+
+#[test]
+fn deleted_view_retains_shared_author_and_cutoff_evidence() {
+ let corpus = lifecycle_corpus();
+ assert_ne!(corpus.author, corpus.other_author);
+ let target_json = lifecycle_json(&corpus, "at_cutoff");
+ let target = verified(target_json);
+ let exact = deletion(&corpus, "combined_cutoff");
+ let address = deletion(&corpus, "address_cutoff");
+ let forged = deletion(&corpus, "forged_address");
+ assert_eq!(forged.event().author().to_hex(), corpus.other_author);
+ for permutation in lifecycle::ALL_THREE_PERMUTATIONS {
+ let source = [&exact, &address, &forged];
+ let requests: Vec<_> = permutation
+ .into_iter()
+ .map(|index| (*source[index]).clone())
+ .collect();
+ let expected =
+ evaluate_nip09_suppression_from_borrowed_requests_v1(&target, requests.iter());
+ let head = AvailabilityHeadView::from_selected(
+ selected(&[&target]),
+ version_view(target_json),
+ &requests,
+ )
+ .expect("shared evidence retained");
+ assert_eq!(head.state(), AvailabilityHeadState::Deleted);
+ let decision = head.suppression().expect("canonical decision");
+ assert_eq!(decision, &expected);
+ assert_eq!(
+ decision.outcome(),
+ RadrootsNip09SuppressionOutcome::Suppressed
+ );
+ assert_eq!(
+ decision.reason(),
+ RadrootsNip09SuppressionReason::EventIdAndAddressReference
+ );
+ assert_eq!(
+ decision
+ .event_reference()
+ .expect("exact evidence")
+ .request_id(),
+ exact.event().id()
+ );
+ let evidence = decision.address_reference().expect("address evidence");
+ assert_eq!(
+ evidence.coordinate().as_str(),
+ format!("30402:{}:{}", corpus.author, lifecycle::IDENTIFIER)
+ );
+ assert_eq!(evidence.inclusive_cutoff(), lifecycle::CUTOFF);
+ assert_eq!(
+ evidence.request_id(),
+ std::cmp::min(exact.event().id(), address.event().id())
+ );
+ }
+ let new_json = lifecycle_json(&corpus, "newer_sold");
+ let new = verified(new_json);
+ let forged_only = AvailabilityHeadView::from_selected(
+ selected(&[&new]),
+ version_view(new_json),
+ &[deletion(&corpus, "forged_exact")],
+ )
+ .expect("request admission is not author authorization");
+ assert_eq!(forged_only.state(), AvailabilityHeadState::Focused);
+ assert_eq!(
+ forged_only
+ .suppression()
+ .expect("author mismatch evidence")
+ .reason(),
+ RadrootsNip09SuppressionReason::RequestAuthorMismatch
+ );
+ for (name, expected_state, expected_reason) in [
+ (
+ "before_cutoff",
+ AvailabilityHeadState::Deleted,
+ RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff,
+ ),
+ (
+ "at_cutoff",
+ AvailabilityHeadState::Deleted,
+ RadrootsNip09SuppressionReason::AddressReferenceAtOrBeforeCutoff,
+ ),
+ (
+ "after_cutoff",
+ AvailabilityHeadState::Focused,
+ RadrootsNip09SuppressionReason::AddressCutoffPrecedesTarget,
+ ),
+ ] {
+ let json = lifecycle_json(&corpus, name);
+ let head = AvailabilityHeadView::from_selected(
+ selected(&[&verified(json)]),
+ version_view(json),
+ std::slice::from_ref(&address),
+ )
+ .expect("inclusive shared address cutoff");
+ assert_eq!(head.state(), expected_state);
+ assert_eq!(
+ head.suppression().expect("cutoff decision").reason(),
+ expected_reason
+ );
+ }
+ // Dedicated capacity boundary: at most4097 duplicate public requests and16MiB
+ // of admitted fixture inputs; production must borrow this slice, never clone it.
+ let request_bytes = lifecycle_json(&corpus, "address_cutoff").len();
+ assert!(request_bytes * (MAX_DELETION_REQUESTS + 1) <= 16 * 1024 * 1024);
+ let mut requests = vec![address; MAX_DELETION_REQUESTS];
+ assert_eq!(requests.len(), 4096);
+ assert_eq!(
+ AvailabilityHeadView::from_selected(
+ selected(&[&target]),
+ version_view(target_json),
+ &requests
+ )
+ .expect("exact request capacity")
+ .state(),
+ AvailabilityHeadState::Deleted
+ );
+ requests.push(exact);
+ assert_eq!(requests.len(), 4097);
+ assert_safe(
+ AvailabilityHeadView::from_selected(
+ selected(&[&target]),
+ version_view(target_json),
+ &requests,
+ )
+ .expect_err("one request beyond capacity"),
+ target_json,
+ );
+}
+
+#[test]
+fn invalid_signatures_cannot_create_verified_views() {
+ let fixture = public_fixture(admission::focused_recipe());
+ let wire = Nip01EventWire::parse_json_unverified(&fixture.json).expect("public base wire");
+ let invalid_signature = replace_string_field(&fixture.json, "sig", &wire.sig, &"0".repeat(128));
+ let unverified = Nip01EventWire::parse_json_unverified(&invalid_signature)
+ .expect("structurally valid invalid signature")
+ .into_unverified_envelope()
+ .expect("unverified envelope");
+ assert_eq!(
+ verify_nip01_event(unverified).expect_err("signature gate owns verified typestate"),
+ RadrootsNip01VerificationError::SignatureInvalid
+ );
+ assert_safe(
+ AvailabilityVersionView::from_verified(
+ verified(&fixture.json),
+ &invalid_signature,
+ observation(),
+ )
+ .expect_err("invalid signature wire cannot bind another verified event"),
+ &invalid_signature,
+ );
+ let replacement = if wire.id.starts_with('0') { '1' } else { '0' };
+ let bad_id = format!("{replacement}{}", &wire.id[1..]);
+ let invalid_id = replace_string_field(&fixture.json, "id", &wire.id, &bad_id);
+ let envelope = Nip01EventWire::parse_json_unverified(&invalid_id)
+ .expect("invalid ID is not structurally trusted")
+ .into_unverified_envelope()
+ .expect("unverified envelope");
+ assert!(matches!(
+ verify_nip01_event(envelope),
+ Err(RadrootsNip01VerificationError::IdMismatch { .. })
+ ));
+}
+
+#[test]
+fn original_wire_must_match_the_verified_event() {
+ let fixture = public_fixture(admission::focused_recipe());
+ let neighbor = public_fixture(admission::focused_recipe());
+ let event = verified(&fixture.json);
+ let wire = Nip01EventWire::parse_json_unverified(&fixture.json).expect("public wire fields");
+ let neighbor_wire = Nip01EventWire::parse_json_unverified(&neighbor.json)
+ .expect("distinct valid public fields");
+ let mismatches = [
+ replace_string_field(&fixture.json, "id", &wire.id, &neighbor_wire.id),
+ replace_string_field(&fixture.json, "pubkey", &wire.pubkey, &neighbor.author),
+ replace_once(
+ &fixture.json,
+ &format!("\"created_at\":{}", admission::CREATED_AT),
+ "\"created_at\":1800000101",
+ ),
+ replace_once(&fixture.json, "\"kind\":30402", "\"kind\":30023"),
+ replace_once(
+ &fixture.json,
+ "[\"status\",\"active\"]",
+ "[\"status\",\"sold\"]",
+ ),
+ replace_string_field(
+ &fixture.json,
+ "content",
+ &wire.content,
+ "Different public content.",
+ ),
+ replace_string_field(&fixture.json, "sig", &wire.sig, &neighbor_wire.sig),
+ ];
+ for mismatch in &mismatches {
+ let parsed = Nip01EventWire::parse_json_unverified(mismatch)
+ .expect("each altered field stays default structurally valid")
+ .into_unverified_envelope()
+ .expect("unverified altered envelope");
+ assert_ne!(&parsed, event.event());
+ assert_safe(
+ AvailabilityVersionView::from_verified(event.clone(), mismatch, observation())
+ .expect_err("full envelope equality required"),
+ mismatch,
+ );
+ }
+ assert_eq!(mismatches.len(), 7);
+ for extras in ["\"untrusted\":true", "\"untrusted\":false"] {
+ let exact = format!("\n {} \t", add_extras(&fixture.json, extras));
+ let retained = AvailabilityVersionView::from_verified(event.clone(), &exact, observation())
+ .expect("unknown extras and formatting remain unauthenticated");
+ assert_eq!(retained.original_json(), exact);
+ assert_eq!(retained.version().event_id(), *event.event().id());
+ assert_eq!(
+ retained
+ .focused()
+ .expect("extras cannot change quantity")
+ .quantity(),
+ None
+ );
+ }
+}
+
+#[test]
+fn view_wire_and_source_retention_obey_shared_byte_limits() {
+ let mut recipe = admission::focused_recipe();
+ recipe.content = "Public wire byte boundary é🥕e\u{301}.".into();
+ let fixture = public_fixture(recipe);
+ assert!(fixture.json.chars().count() < fixture.json.len());
+ let event = verified(&fixture.json);
+ assert_eq!(DEFAULT_RAW_JSON_MAX_BYTES, 256 * 1024);
+ // Dedicated raw-wire exception to the16KiB signed-fixture bound: whitespace
+ // padding creates exactly256KiB and256KiB+1 without enlarging signed fields.
+ let exact = format!(
+ "{}{}",
+ fixture.json,
+ " ".repeat(DEFAULT_RAW_JSON_MAX_BYTES - fixture.json.len())
+ );
+ assert_eq!(exact.len(), DEFAULT_RAW_JSON_MAX_BYTES);
+ assert_eq!(
+ AvailabilityVersionView::from_verified(event.clone(), &exact, observation())
+ .expect("exact raw-wire capacity")
+ .original_json()
+ .as_bytes(),
+ exact.as_bytes()
+ );
+ let over = exact + " ";
+ assert_eq!(over.len(), DEFAULT_RAW_JSON_MAX_BYTES + 1);
+ assert_safe(
+ AvailabilityVersionView::from_verified(event.clone(), &over, observation())
+ .expect_err("raw capacity plus one"),
+ "hcav-incoming-v1",
+ );
+ let invalid_tags = replace_once(&fixture.json, "\"tags\":[", "\"tags\":[[0],");
+ let overlong_tag = replace_once(
+ &fixture.json,
+ "[\"status\",\"active\"]",
+ &format!(
+ "[\"status\",\"{}\"]",
+ "x".repeat(DEFAULT_TAG_ELEMENT_MAX_BYTES + 1)
+ ),
+ );
+ let too_many_fields = (0..=DEFAULT_EXTRA_MAX_FIELDS)
+ .map(|index| format!("\"extra_{index}\":0"))
+ .collect::<Vec<_>>()
+ .join(",");
+ let oversized_extra = format!(
+ "\"extra\":\"{}\"",
+ "x".repeat(DEFAULT_EXTRA_TOTAL_JSON_MAX_BYTES + 1)
+ );
+ for invalid in [
+ invalid_tags,
+ overlong_tag,
+ add_extras(&fixture.json, &too_many_fields),
+ add_extras(&fixture.json, &oversized_extra),
+ ] {
+ assert!(invalid.len() <= DEFAULT_RAW_JSON_MAX_BYTES);
+ assert!(
+ Nip01EventWire::parse_json_unverified(&invalid).is_err(),
+ "shared default structural boundary"
+ );
+ assert_safe(
+ AvailabilityVersionView::from_verified(event.clone(), &invalid, observation())
+ .expect_err("shared limits precede retention"),
+ &invalid,
+ );
+ }
+ let other_kind = public_fixture_at(
+ admission::focused_recipe(),
+ lifecycle::OTHER_ADDRESSABLE_KIND,
+ admission::CREATED_AT,
+ );
+ assert_safe(
+ AvailabilityVersionView::from_verified(
+ verified(&other_kind.json),
+ &other_kind.json,
+ observation(),
+ )
+ .expect_err("view only supports kind30402"),
+ &other_kind.json,
+ );
+ for unit in ["x", "é", "🥕", "e\u{301}"] {
+ let prefix = "wss://relay.example.test/source/";
+ let source = prefix.to_owned() + &text_at_bytes(unit, MAX_SOURCE_BYTES - prefix.len());
+ assert_eq!(source.len(), MAX_SOURCE_BYTES);
+ let observed = AvailabilityObservation::parse(&source, UnixTimestamp::UNIX_EPOCH)
+ .expect("exact UTF-8 source byte bound");
+ assert_eq!(observed.source().as_str().as_bytes(), source.as_bytes());
+ let over = source + "x";
+ assert_eq!(over.len(), MAX_SOURCE_BYTES + 1);
+ assert_safe(
+ AvailabilityObservation::parse(&over, UnixTimestamp::UNIX_EPOCH)
+ .expect_err("source byte bound plus one"),
+ &over,
+ );
+ }
+}
+
+#[test]
+fn view_errors_never_echo_untrusted_input() {
+ const PAYLOAD: &str = "private-debug-payload-marker";
+ let mut recipe = admission::focused_recipe();
+ recipe.content = PAYLOAD.into();
+ recipe
+ .tags
+ .iter_mut()
+ .find(|tag| tag[0] == "d")
+ .expect("identifier")[1] = format!("{PAYLOAD}:\0 ");
+ let fixture = public_fixture(recipe);
+ let source = format!("wss://relay.example.test/{PAYLOAD}?metadata={PAYLOAD}");
+ let observation = AvailabilityObservation::parse(&source, UnixTimestamp::UNIX_EPOCH)
+ .expect("public opaque source");
+ let version = AvailabilityVersionView::from_verified(
+ verified(&fixture.json),
+ &fixture.json,
+ observation.clone(),
+ )
+ .expect("unsupported evidence view");
+ let metadata = ProfileMetadata::new(
+ Some(PAYLOAD.into()),
+ None,
+ None,
+ Some(PAYLOAD.into()),
+ Some(format!("https://images.example.test/{PAYLOAD}")),
+ )
+ .expect("bounded display fields");
+ let candidate = Kind0ProfileCandidate::new(
+ EventId::from_bytes([7; 32]),
+ PublicKey::from_hex(&fixture.author).expect("author"),
+ UnixTimestamp::UNIX_EPOCH,
+ metadata,
+ );
+ let attached = version
+ .with_profile(Some(&candidate))
+ .expect("caller-associated display metadata");
+ let head = AvailabilityHeadView::from_selected(
+ selected(&[&verified(&fixture.json)]),
+ attached.clone(),
+ &[],
+ )
+ .expect("unsupported head");
+ let missing = AvailabilityHeadView::missing(
+ AvailabilityListingCoordinate::parse(&format!("30402:{}:{PAYLOAD}", fixture.author))
+ .expect("opaque missing reference"),
+ );
+ for debug in [
+ format!("{observation:?}"),
+ format!("{version:?}"),
+ format!("{attached:?}"),
+ format!("{head:?}"),
+ format!("{missing:?}"),
+ ] {
+ assert!(debug.len() <= 1024, "bounded safe summary");
+ for sensitive in [PAYLOAD, "wss://", "https://", "\"tags\"", "\"sig\""] {
+ assert!(
+ !debug.contains(sensitive),
+ "debug cannot expose raw payloads"
+ );
+ }
+ }
+ for source in [
+ format!("https://relay.example.test/{PAYLOAD}"),
+ format!("wss://user:{PAYLOAD}@relay.example.test"),
+ format!("wss://relay.example.test/#{PAYLOAD}"),
+ format!("wss://relay.example.test/\n{PAYLOAD}"),
+ ] {
+ assert_safe(
+ AvailabilityObservation::parse(&source, UnixTimestamp::UNIX_EPOCH)
+ .expect_err("shared source validation returns static errors"),
+ PAYLOAD,
+ );
+ }
+ let malformed = format!("{{{PAYLOAD}");
+ assert_safe(
+ AvailabilityVersionView::from_verified(verified(&fixture.json), &malformed, observation)
+ .expect_err("malformed wire safe error"),
+ PAYLOAD,
+ );
+}
+
+#[test]
+fn optional_profile_attachment_checks_publisher_and_preserves_missing_fields() {
+ let fixture = public_fixture(admission::focused_recipe());
+ let view = version_view(&fixture.json);
+ assert_eq!(view.profile_metadata(), None);
+ let metadata = ProfileMetadata::new(None, Some(" Caller display ".into()), None, None, None)
+ .expect("bounded optional display metadata");
+ // This constructor has no signature typestate. The association proves only
+ // matching author and bounded display data, not verified profile or ownership.
+ let candidate = Kind0ProfileCandidate::new(
+ EventId::from_bytes([9; 32]),
+ PublicKey::from_hex(&fixture.author).expect("listing author"),
+ UnixTimestamp::UNIX_EPOCH,
+ metadata.clone(),
+ );
+ let attached = view
+ .with_profile(Some(&candidate))
+ .expect("same-author caller display metadata");
+ assert_eq!(attached.profile_metadata(), Some(&metadata));
+ let attached_metadata = attached.profile_metadata().expect("attached fields");
+ assert_eq!(attached_metadata.display_name(), Some("Caller display"));
+ assert_eq!(attached_metadata.name(), None);
+ assert_eq!(attached_metadata.nip05(), None);
+ assert_eq!(attached_metadata.about(), None);
+ assert_eq!(attached_metadata.picture(), None);
+ assert_eq!(view.profile_metadata(), None, "immutable original");
+ assert_eq!(attached.version(), view.version());
+ assert_eq!(attached.focused(), view.focused());
+ assert_eq!(attached.original_json(), view.original_json());
+ assert_eq!(
+ attached
+ .with_profile(None)
+ .expect("explicit absent optional metadata")
+ .profile_metadata(),
+ None
+ );
+ let other = public_fixture(admission::focused_recipe());
+ assert_ne!(other.author, fixture.author);
+ let wrong = Kind0ProfileCandidate::new(
+ EventId::from_bytes([8; 32]),
+ PublicKey::from_hex(&other.author).expect("other public author"),
+ UnixTimestamp::UNIX_EPOCH,
+ metadata,
+ );
+ assert_safe(
+ view.with_profile(Some(&wrong))
+ .expect_err("author mismatch before metadata copy"),
+ "Caller display",
+ );
+ assert_eq!(
+ view.focused()
+ .expect("failed optional attachment preserves listing")
+ .status()
+ .as_str(),
+ "active"
+ );
+ assert_eq!(
+ view.version(),
+ AvailabilityEventVersion::from_canonical(*verified(&fixture.json).event().id())
+ );
+}
+
+#[test]
+fn observation_preserves_bounded_source_reference_and_observation_time() {
+ let source = "wss://Relay.Example.test:443/path?region=ca-bc";
+ let local = UnixTimestamp::from_seconds(i64::MAX).expect("local timestamp upper bound");
+ let observation = AvailabilityObservation::parse(source, local)
+ .expect("pure shared validation preserves supplied spelling");
+ assert_eq!(observation.source().as_str(), source);
+ assert_eq!(observation.observed_at(), local);
+ assert_ne!(
+ observation.source().as_str(),
+ "wss://relay.example.test/path?region=ca-bc"
+ );
+ let empty_time =
+ AvailabilityObservation::parse("ws://127.0.0.1:8080/isolated", UnixTimestamp::UNIX_EPOCH)
+ .expect("provenance is not endpoint authorization");
+ assert_eq!(empty_time.observed_at().as_seconds(), 0);
+ for invalid in [
+ "",
+ "http://relay.example.test",
+ "WSS://relay.example.test",
+ " wss://relay.example.test",
+ "wss://",
+ "wss://relay.example.test:0",
+ ] {
+ let error = AvailabilityObservation::parse(invalid, local)
+ .expect_err("delegate selected shared pure source validation");
+ assert_eq!(error.code(), SafeErrorCode::InvalidProfileMetadata);
+ assert!(error.to_string().len() <= 128);
+ }
+}
diff --git a/radroots.lib.source-lock.v1.toml b/radroots.lib.source-lock.v1.toml
@@ -6,4 +6,4 @@ workspace_catalog_sha256 = "ee295f2352e2577a4052d980624415aec9871197d4fc9910a4c2
version = "0.1.0-alpha"
source_archive_sha256 = "c648a3ab993d10253b9073e7e86db7b8970863bdf1d394d9fc30d66825695240"
lockfile = "core/Cargo.lock"
-lockfile_sha256 = "1cf1c2d3f60883fbfe8a336fd0e20cb755b79f29a241324d1832d1e819b2cebd"
+lockfile_sha256 = "049aea164146e45c41a4a9d9686cca4a9ca88413d8a5b17aaec78b2bef0947bc"
diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs
@@ -1159,7 +1159,7 @@ fn provenance_check(root: &Path, inventory: &Inventory, findings: &mut Vec<Strin
"version = \"0.1.0-alpha\"\n",
"source_archive_sha256 = \"c648a3ab993d10253b9073e7e86db7b8970863bdf1d394d9fc30d66825695240\"\n",
"lockfile = \"core/Cargo.lock\"\n",
- "lockfile_sha256 = \"1cf1c2d3f60883fbfe8a336fd0e20cb755b79f29a241324d1832d1e819b2cebd\"\n",
+ "lockfile_sha256 = \"049aea164146e45c41a4a9d9686cca4a9ca88413d8a5b17aaec78b2bef0947bc\"\n",
);
let source_lock_bytes =
match bounded_no_follow_bytes(root, Path::new(SOURCE_LOCK_PATH), MAX_SOURCE_LOCK_BYTES) {
@@ -2434,7 +2434,7 @@ mod tests {
"version = \"0.1.0-alpha\"\n",
"source_archive_sha256 = \"c648a3ab993d10253b9073e7e86db7b8970863bdf1d394d9fc30d66825695240\"\n",
"lockfile = \"core/Cargo.lock\"\n",
- "lockfile_sha256 = \"1cf1c2d3f60883fbfe8a336fd0e20cb755b79f29a241324d1832d1e819b2cebd\"\n",
+ "lockfile_sha256 = \"049aea164146e45c41a4a9d9686cca4a9ca88413d8a5b17aaec78b2bef0947bc\"\n",
),
);
write(&root, "core/Cargo.toml", "");