lib

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

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:
Mcrates/nostr/src/error.rs | 6++++++
Mcrates/nostr/src/event.rs | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/nostr/src/filter.rs | 25+++++++++++++++----------
Mcrates/nostr/src/lib.rs | 35++++++++++++++++++++++++-----------
Mcrates/nostr/src/tag.rs | 48++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/nostr/src/types.rs | 21+++++++++++----------
Mcrates/nostr/tests/blossom_conformance.rs | 9++++++---
Mcrates/nostr/tests/coverage.rs | 21+++++++++++----------
Mcrates/nostr/tests/package_boundary.rs | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/nostr/tests/post_profile.rs | 3++-
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]