commit 3978d830ac861e0bbec4e7fa05b4ff8ec60fc847
parent b943892c0c085e839d289c000b7b8be1907bc2aa
Author: triesap <tyson@radroots.org>
Date: Fri, 31 Jul 2026 10:35:24 +0000
nostr: normalize event, tag, filter, and coordinate adapters
- Move protocol value aliases into explicit event, tag, and filter modules.
- Add deterministic coordinate and tag conversions with normalized errors.
- Keep typed authoring controls while privatizing predecessor implementations.
- Add API boundary regressions and migrate direct package callers.
Diffstat:
10 files changed, 251 insertions(+), 51 deletions(-)
diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs
@@ -31,6 +31,12 @@ pub enum RadrootsNostrError {
#[error("Radroots event field {field} cannot be represented by Nostr")]
EventConversion { field: &'static str },
+ #[error("Radroots event coordinate cannot be represented by Nostr")]
+ CoordinateConversion,
+
+ #[error("Radroots event tag cannot be represented by Nostr")]
+ TagConversion,
+
#[error("Nostr event kind {kind} requires typed authoring")]
TypedAuthoringRequired { kind: u16 },
diff --git a/crates/nostr/src/event.rs b/crates/nostr/src/event.rs
@@ -1,5 +1,68 @@
//! Portable conversion between Radroots events and Nostr protocol events.
//!
-//! The final conversion surface is populated by Steps 125-126. This module is
-//! established at its durable public path before behavior moves out of the
-//! predecessor modules.
+//! Upstream protocol values remain confined to this explicit adapter boundary;
+//! canonical Radroots event and coordinate values cross it through validated,
+//! deterministic conversions.
+
+use alloc::string::ToString;
+
+use radroots_event::id::Nip01Coordinate;
+
+use crate::Error;
+
+/// Upstream Nostr coordinate used only at the explicit protocol boundary.
+pub type Coordinate = nostr::nips::nip01::Coordinate;
+/// Upstream Nostr event used only at the explicit protocol boundary.
+pub type Event = nostr::Event;
+/// Upstream Nostr event identifier used only at the explicit protocol boundary.
+pub type EventId = nostr::EventId;
+/// Upstream Nostr event kind used only at the explicit protocol boundary.
+pub type Kind = nostr::Kind;
+/// Upstream Nostr metadata used only at the explicit protocol boundary.
+pub type Metadata = nostr::Metadata;
+/// Upstream Nostr timestamp used only at the explicit protocol boundary.
+pub type Timestamp = nostr::Timestamp;
+
+#[cfg(feature = "events")]
+pub use crate::event_convert::{
+ nostr_event_from_radroots, radroots_event_from_nostr, radroots_event_ptr_from_nostr,
+};
+
+/// Converts a canonical Radroots NIP-01 coordinate to its Nostr value.
+pub fn coordinate_to_nostr(coordinate: &Nip01Coordinate) -> Result<Coordinate, Error> {
+ Coordinate::from_kpi_format(coordinate.as_str()).map_err(|_| Error::CoordinateConversion)
+}
+
+/// Converts a Nostr coordinate to the canonical Radroots NIP-01 value.
+pub fn coordinate_from_nostr(coordinate: &Coordinate) -> Result<Nip01Coordinate, Error> {
+ Nip01Coordinate::parse(coordinate.to_string()).map_err(|_| Error::CoordinateConversion)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+
+ #[test]
+ fn coordinate_round_trip_is_canonical_and_rejects_invalid_kinds() {
+ let native = Nip01Coordinate::parse(format!("30402:{PUBLIC_KEY}:listing-1"))
+ .expect("native coordinate");
+ let nostr = coordinate_to_nostr(&native).expect("Nostr coordinate");
+
+ assert_eq!(nostr.to_string(), native.as_str());
+ assert_eq!(
+ coordinate_from_nostr(&nostr).expect("native coordinate"),
+ native
+ );
+
+ let invalid = Coordinate::new(
+ Kind::TextNote,
+ nostr::PublicKey::from_hex(PUBLIC_KEY).expect("public key"),
+ );
+ assert!(matches!(
+ coordinate_from_nostr(&invalid),
+ Err(Error::CoordinateConversion)
+ ));
+ }
+}
diff --git a/crates/nostr/src/filter.rs b/crates/nostr/src/filter.rs
@@ -1,14 +1,19 @@
+//! Explicit construction of portable Nostr subscription filters.
+
use crate::error::RadrootsNostrError;
+use crate::event::Kind;
#[cfg(feature = "std")]
-use crate::types::RadrootsNostrTimestamp;
-use crate::types::{RadrootsNostrFilter, RadrootsNostrKind};
+use crate::event::Timestamp;
use alloc::{string::String, string::ToString, vec::Vec};
+/// Upstream Nostr filter used only at the explicit protocol boundary.
+pub type Filter = nostr::Filter;
+
pub fn radroots_nostr_filter_tag(
- filter: RadrootsNostrFilter,
+ filter: Filter,
tag: &str,
values: Vec<String>,
-) -> Result<RadrootsNostrFilter, RadrootsNostrError> {
+) -> Result<Filter, RadrootsNostrError> {
let mut chars = tag.chars();
let tag_char = chars
.next()
@@ -23,15 +28,15 @@ pub fn radroots_nostr_filter_tag(
Ok(filter.custom_tags(tag_key, values))
}
-pub fn radroots_nostr_kind(kind: u16) -> RadrootsNostrKind {
- RadrootsNostrKind::Custom(kind)
+pub fn radroots_nostr_kind(kind: u16) -> Kind {
+ Kind::Custom(kind)
}
-pub fn radroots_nostr_filter_kind(kind: u16) -> RadrootsNostrFilter {
- RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(kind))
+pub fn radroots_nostr_filter_kind(kind: u16) -> Filter {
+ Filter::new().kind(Kind::Custom(kind))
}
#[cfg(feature = "std")]
-pub fn radroots_nostr_filter_new_events(filter: RadrootsNostrFilter) -> RadrootsNostrFilter {
- filter.since(RadrootsNostrTimestamp::now())
+pub fn radroots_nostr_filter_new_events(filter: Filter) -> Filter {
+ filter.since(Timestamp::now())
}
diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs
@@ -13,7 +13,7 @@ pub mod events;
pub mod filter;
pub mod key;
pub mod tag;
-pub mod tags;
+mod tags;
pub mod types;
pub mod util;
@@ -40,7 +40,7 @@ pub mod event_adapters;
#[cfg(feature = "events")]
pub mod draft_signing;
#[cfg(feature = "events")]
-pub mod event_convert;
+mod event_convert;
#[cfg(feature = "events")]
pub mod event_verify;
@@ -105,21 +105,34 @@ pub mod prelude {
radroots_nostr_metadata_has_fields,
};
+ pub use crate::event::{
+ Coordinate as RadrootsNostrCoordinate, Event as RadrootsNostrEvent,
+ EventId as RadrootsNostrEventId, Kind as RadrootsNostrKind,
+ Metadata as RadrootsNostrMetadata, Timestamp as RadrootsNostrTimestamp,
+ };
+ pub use crate::filter::Filter as RadrootsNostrFilter;
#[cfg(feature = "nip17")]
pub use crate::nip17::{
RadrootsNip17Error, RadrootsNip17Rumor, RadrootsNip17WrapOptions,
radroots_nostr_unwrap_gift_wrap, radroots_nostr_wrap_message,
radroots_nostr_wrap_message_file,
};
- pub use crate::tags::*;
+ pub use crate::tag::{
+ Tag as RadrootsNostrTag, TagKind as RadrootsNostrTagKind,
+ TagStandard as RadrootsNostrTagStandard,
+ };
+ pub use crate::tag::{
+ radroots_nostr_tag_at_value, radroots_nostr_tag_first_value,
+ radroots_nostr_tag_match_geohash, radroots_nostr_tag_match_l,
+ radroots_nostr_tag_match_location, radroots_nostr_tag_match_summary,
+ radroots_nostr_tag_match_title, radroots_nostr_tag_relays_parse, radroots_nostr_tag_slice,
+ radroots_nostr_tags_match, radroots_nostr_tags_resolve,
+ };
pub use crate::types::{
- RadrootsNostrCoordinate, RadrootsNostrEvent, RadrootsNostrEventId,
- RadrootsNostrExternalSigningRequest, RadrootsNostrFilter, RadrootsNostrFromBech32,
- RadrootsNostrGenericEventBuilder, RadrootsNostrKeys, RadrootsNostrKind,
- RadrootsNostrMetadata, RadrootsNostrPublicKey, RadrootsNostrRelayUrl,
- RadrootsNostrSecp256k1SecretKey, RadrootsNostrSecretKey, RadrootsNostrSubscriptionId,
- RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp,
- RadrootsNostrToBech32, RadrootsNostrUrl,
+ RadrootsNostrExternalSigningRequest, RadrootsNostrFromBech32,
+ RadrootsNostrGenericEventBuilder, RadrootsNostrKeys, RadrootsNostrPublicKey,
+ RadrootsNostrRelayUrl, RadrootsNostrSecp256k1SecretKey, RadrootsNostrSecretKey,
+ RadrootsNostrSubscriptionId, RadrootsNostrToBech32, RadrootsNostrUrl,
};
#[cfg(feature = "events")]
@@ -129,7 +142,7 @@ pub mod prelude {
pub use crate::draft_signing::radroots_nostr_sign_frozen_draft;
#[cfg(feature = "events")]
- pub use crate::event_convert::{
+ pub use crate::event::{
nostr_event_from_radroots, radroots_event_from_nostr, radroots_event_ptr_from_nostr,
};
diff --git a/crates/nostr/src/tag.rs b/crates/nostr/src/tag.rs
@@ -1,4 +1,48 @@
//! Portable Nostr tag conversion and validation.
//!
-//! Step 126 consolidates the predecessor tag helpers at this durable public
-//! path.
+//! Ordered protocol-neutral tag parts cross the upstream Nostr boundary only
+//! through this module's validated conversion functions.
+
+use alloc::{string::String, vec::Vec};
+
+use crate::Error;
+
+/// Upstream Nostr tag used only at the explicit protocol boundary.
+pub type Tag = nostr::Tag;
+/// Upstream Nostr tag kind used only at the explicit protocol boundary.
+pub type TagKind<'a> = nostr::TagKind<'a>;
+/// Upstream standardized Nostr tag used only at the explicit protocol boundary.
+pub type TagStandard = nostr::TagStandard;
+
+pub use crate::tags::{
+ radroots_nostr_tag_at_value, radroots_nostr_tag_first_value, radroots_nostr_tag_match_geohash,
+ radroots_nostr_tag_match_l, radroots_nostr_tag_match_location,
+ radroots_nostr_tag_match_summary, radroots_nostr_tag_match_title,
+ radroots_nostr_tag_relays_parse, radroots_nostr_tag_slice, radroots_nostr_tags_match,
+ radroots_nostr_tags_resolve,
+};
+
+/// Parses canonical tag parts into the explicit Nostr tag boundary value.
+pub fn from_parts(parts: Vec<String>) -> Result<Tag, Error> {
+ Tag::parse(parts).map_err(|_| Error::TagConversion)
+}
+
+/// Copies a Nostr tag into its protocol-neutral ordered string parts.
+#[must_use]
+pub fn to_parts(tag: &Tag) -> Vec<String> {
+ tag.as_slice().to_vec()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn tag_parts_round_trip_without_upstream_error_leakage() {
+ let parts = vec!["t".to_owned(), "soil".to_owned()];
+ let tag = from_parts(parts.clone()).expect("tag");
+
+ assert_eq!(to_parts(&tag), parts);
+ assert!(matches!(from_parts(Vec::new()), Err(Error::TagConversion)));
+ }
+}
diff --git a/crates/nostr/src/types.rs b/crates/nostr/src/types.rs
@@ -7,22 +7,23 @@ use radroots_event::listing::classified::{
ClassifiedListingPartition, classify_classified_listing_marker_names,
};
-pub type RadrootsNostrCoordinate = nostr::nips::nip01::Coordinate;
-pub type RadrootsNostrEvent = nostr::Event;
+#[cfg(feature = "events")]
+pub(crate) use crate::event::Metadata as RadrootsNostrMetadata;
+pub(crate) use crate::event::{
+ Event as RadrootsNostrEvent, EventId as RadrootsNostrEventId, Kind as RadrootsNostrKind,
+ Timestamp as RadrootsNostrTimestamp,
+};
+pub(crate) use crate::filter::Filter as RadrootsNostrFilter;
+pub(crate) use crate::tag::{
+ Tag as RadrootsNostrTag, TagKind as RadrootsNostrTagKind,
+ TagStandard as RadrootsNostrTagStandard,
+};
pub(crate) type RadrootsNostrEventBuilderUnchecked = nostr::EventBuilder;
-pub type RadrootsNostrEventId = nostr::EventId;
-pub type RadrootsNostrFilter = nostr::Filter;
-pub type RadrootsNostrKind = nostr::Kind;
pub type RadrootsNostrKeys = nostr::Keys;
-pub type RadrootsNostrMetadata = nostr::Metadata;
pub type RadrootsNostrPublicKey = nostr::PublicKey;
pub type RadrootsNostrRelayUrl = nostr::RelayUrl;
pub type RadrootsNostrSecretKey = nostr::SecretKey;
pub type RadrootsNostrSubscriptionId = nostr::SubscriptionId;
-pub type RadrootsNostrTag = nostr::Tag;
-pub type RadrootsNostrTagKind<'a> = nostr::TagKind<'a>;
-pub type RadrootsNostrTagStandard = nostr::TagStandard;
-pub type RadrootsNostrTimestamp = nostr::Timestamp;
pub type RadrootsNostrUrl = nostr::Url;
/// A checked generic event prepared for an external signer.
diff --git a/crates/nostr/tests/blossom_conformance.rs b/crates/nostr/tests/blossom_conformance.rs
@@ -14,9 +14,12 @@ use radroots_nostr::blossom::{
RadrootsNostrBlossomError, radroots_nostr_decode_verify_blossom_authorization_header,
radroots_nostr_encode_blossom_authorization_header, radroots_nostr_sign_blossom_authorization,
};
-use radroots_nostr::types::{
- RadrootsNostrEvent, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrTag,
- RadrootsNostrTimestamp,
+use radroots_nostr::{
+ event::{
+ Event as RadrootsNostrEvent, Kind as RadrootsNostrKind, Timestamp as RadrootsNostrTimestamp,
+ },
+ tag::Tag as RadrootsNostrTag,
+ types::RadrootsNostrKeys,
};
use serde::Deserialize;
use serde_json::Value;
diff --git a/crates/nostr/tests/coverage.rs b/crates/nostr/tests/coverage.rs
@@ -18,18 +18,19 @@ use radroots_nostr::filter::{
radroots_nostr_kind,
};
use radroots_nostr::key::{parse_public_key, public_key_from_nostr, public_key_to_npub};
-use radroots_nostr::tags::{
- radroots_nostr_tag_at_value, radroots_nostr_tag_first_value, radroots_nostr_tag_match_geohash,
- radroots_nostr_tag_match_l, radroots_nostr_tag_match_location,
- radroots_nostr_tag_match_summary, radroots_nostr_tag_match_title,
- radroots_nostr_tag_relays_parse, radroots_nostr_tag_slice, radroots_nostr_tags_match,
- radroots_nostr_tags_resolve,
-};
-use radroots_nostr::types::{
- RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrRelayUrl, RadrootsNostrTag,
- RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp,
+use radroots_nostr::tag::{
+ Tag as RadrootsNostrTag, TagKind as RadrootsNostrTagKind,
+ TagStandard as RadrootsNostrTagStandard, radroots_nostr_tag_at_value,
+ radroots_nostr_tag_first_value, radroots_nostr_tag_match_geohash, radroots_nostr_tag_match_l,
+ radroots_nostr_tag_match_location, radroots_nostr_tag_match_summary,
+ radroots_nostr_tag_match_title, radroots_nostr_tag_relays_parse, radroots_nostr_tag_slice,
+ radroots_nostr_tags_match, radroots_nostr_tags_resolve,
};
use radroots_nostr::util::{created_at_u32_saturating, event_created_at_u32_saturating};
+use radroots_nostr::{
+ event::{Kind as RadrootsNostrKind, Timestamp as RadrootsNostrTimestamp},
+ types::{RadrootsNostrKeys, RadrootsNostrRelayUrl},
+};
use test_fixtures::RELAY_PRIMARY_WSS;
fn make_keys() -> RadrootsNostrKeys {
diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs
@@ -3,11 +3,21 @@ use std::fs;
use std::path::{Path, PathBuf};
#[allow(unused_imports)]
-use radroots_nostr::{Error as _, event as _, filter as _, key as _, tag as _};
+use radroots_nostr::{
+ Error as _,
+ event::{Coordinate as _, Event as _, EventId as _, Kind as _, Metadata as _, Timestamp as _},
+ filter::Filter as _,
+ key as _,
+ tag::{Tag as _, TagKind as _, TagStandard as _},
+};
const MANIFEST: &str = include_str!("../Cargo.toml");
const ROOT: &str = include_str!("../src/lib.rs");
+const EVENT_MODULE: &str = include_str!("../src/event.rs");
+const FILTER_MODULE: &str = include_str!("../src/filter.rs");
const KEY_MODULE: &str = include_str!("../src/key.rs");
+const TAG_MODULE: &str = include_str!("../src/tag.rs");
+const TYPES_MODULE: &str = include_str!("../src/types.rs");
const IDENTITY_MANIFEST: &str = include_str!("../../identity/Cargo.toml");
const IDENTITY_KEY_MODULE: &str = include_str!("../../identity/src/key.rs");
const TRANSPORT_MANIFEST: &str = include_str!("../../transport_nostr/Cargo.toml");
@@ -84,6 +94,59 @@ fn crate_root_establishes_the_final_public_module_skeleton() {
}
#[test]
+fn protocol_values_are_exposed_only_at_explicit_adapter_modules() {
+ for (module, aliases) in [
+ (
+ EVENT_MODULE,
+ [
+ "pub type Coordinate",
+ "pub type Event",
+ "pub type EventId",
+ "pub type Kind",
+ "pub type Metadata",
+ "pub type Timestamp",
+ ]
+ .as_slice(),
+ ),
+ (FILTER_MODULE, ["pub type Filter"].as_slice()),
+ (
+ TAG_MODULE,
+ ["pub type Tag", "pub type TagKind", "pub type TagStandard"].as_slice(),
+ ),
+ ] {
+ for alias in aliases {
+ assert!(
+ module.contains(alias),
+ "explicit adapter module is missing `{alias}`"
+ );
+ }
+ }
+
+ for forbidden in [
+ "pub type RadrootsNostrCoordinate",
+ "pub type RadrootsNostrEvent",
+ "pub type RadrootsNostrEventId",
+ "pub type RadrootsNostrFilter",
+ "pub type RadrootsNostrKind",
+ "pub type RadrootsNostrMetadata",
+ "pub type RadrootsNostrTag",
+ "pub type RadrootsNostrTagKind",
+ "pub type RadrootsNostrTagStandard",
+ "pub type RadrootsNostrTimestamp",
+ ] {
+ assert!(
+ !TYPES_MODULE.contains(forbidden),
+ "broad predecessor alias remains public in types: `{forbidden}`"
+ );
+ }
+
+ assert!(ROOT.contains("mod event_convert;"));
+ assert!(ROOT.contains("mod tags;"));
+ assert!(!ROOT.contains("pub mod event_convert;"));
+ assert!(!ROOT.contains("pub mod tags;"));
+}
+
+#[test]
fn live_client_and_http_ownership_belongs_to_transport_nostr() {
for forbidden in [
"nostr-sdk",
diff --git a/crates/nostr/tests/post_profile.rs b/crates/nostr/tests/post_profile.rs
@@ -9,11 +9,12 @@ use radroots_event::{
},
};
use radroots_nostr::{
+ event::Timestamp as RadrootsNostrTimestamp,
events::post::{
radroots_nostr_build_ask_event, radroots_nostr_build_photo_update_event,
radroots_nostr_build_update_event,
},
- types::{RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp},
+ types::{RadrootsNostrKeys, RadrootsNostrSecretKey},
};
#[test]