commit 639cdae2fc15cab732ff507bb6640d1302d577ae
parent e008273835fdadfeacce80727bbc01f360e718cd
Author: triesap <tyson@radroots.org>
Date: Fri, 31 Jul 2026 13:02:16 +0000
nostr: complete package conformance coverage
- exercise canonical protocol conversions through the public adapter surface
- prove exact authorized local signing through the generic signer contract
- reject forbidden production dependencies traits and host runtime ownership
- qualify every supported feature target documentation and workspace gate
Diffstat:
2 files changed, 173 insertions(+), 1 deletion(-)
diff --git a/crates/nostr/tests/conformance.rs b/crates/nostr/tests/conformance.rs
@@ -0,0 +1,88 @@
+use radroots_event::id::Nip01Coordinate;
+use radroots_identity::PublicKey;
+use radroots_nostr::{
+ Error,
+ event::{coordinate_from_nostr, coordinate_to_nostr},
+ key::{public_key_from_npub, public_key_to_npub},
+ tag::{from_parts, to_parts},
+};
+
+const PUBLIC_KEY: &str = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
+
+#[test]
+fn public_protocol_conversions_are_canonical_and_typed() {
+ let public_key = PublicKey::from_hex(PUBLIC_KEY).expect("canonical public key");
+ let npub = public_key_to_npub(public_key).expect("NIP-19 public key");
+ assert!(npub.starts_with("npub1"));
+ assert_eq!(
+ public_key_from_npub(&npub).expect("canonical public key"),
+ public_key
+ );
+
+ let coordinate = Nip01Coordinate::parse(format!("30402:{PUBLIC_KEY}:listing-1"))
+ .expect("canonical coordinate");
+ let nostr_coordinate = coordinate_to_nostr(&coordinate).expect("Nostr coordinate");
+ assert_eq!(
+ coordinate_from_nostr(&nostr_coordinate).expect("canonical coordinate"),
+ coordinate
+ );
+
+ let parts = vec!["t".to_owned(), "soil".to_owned()];
+ let tag = from_parts(parts.clone()).expect("Nostr tag");
+ assert_eq!(to_parts(&tag), parts);
+ assert!(matches!(from_parts(Vec::new()), Err(Error::TagConversion)));
+}
+
+#[cfg(feature = "signing")]
+#[tokio::test]
+async fn public_local_signer_signs_only_the_exact_authorized_draft() {
+ use radroots_event::{EventDraft, contract::AuthorRole, envelope::kind::KIND_GEOCHAT};
+ use radroots_nostr::{key::SecretKey, signing::LocalSigner};
+ use radroots_protocol::runtime::v1::OperationId;
+ use radroots_signing::{
+ Actor, SignRequest, Signer,
+ actor::ActorSource,
+ request::{CancellationPolicy, SignPolicy},
+ };
+
+ let secret_key =
+ SecretKey::parse("0000000000000000000000000000000000000000000000000000000000000001")
+ .expect("fixture secret");
+ let public_key = secret_key.public_key().expect("public key");
+ assert_eq!(public_key.to_hex(), PUBLIC_KEY);
+
+ let draft = EventDraft::new(
+ "radroots.social.geochat.v1",
+ KIND_GEOCHAT,
+ 1_700_000_000,
+ Vec::new(),
+ "package-conformance-message",
+ PUBLIC_KEY,
+ )
+ .expect("frozen event draft");
+ let expected_id = draft.expected_event_id_hex();
+ let actor = Actor::new(
+ public_key,
+ ActorSource::ExplicitPublicKey,
+ [AuthorRole::Any],
+ )
+ .expect("authorized actor");
+ let request = SignRequest::new(
+ OperationId::SyncPush,
+ actor,
+ draft,
+ SignPolicy::new(u64::MAX, CancellationPolicy::LocalCooperative)
+ .expect("bounded signing policy"),
+ )
+ .expect("signing request");
+
+ let signer = LocalSigner::new(secret_key).expect("local signer");
+ let receipt = signer.sign(request).await.expect("signed receipt");
+
+ assert_eq!(receipt.signed_event().id_str(), expected_id);
+ assert_eq!(*receipt.signed_event().pubkey(), public_key);
+ assert_eq!(
+ receipt.signed_event().content(),
+ "package-conformance-message"
+ );
+}
diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs
@@ -71,7 +71,8 @@ fn manifest_has_final_identity_features_and_radroots_dependencies() {
}
assert_eq!(
dependencies
- .into_iter()
+ .iter()
+ .copied()
.filter(|dependency| dependency.starts_with("radroots_"))
.collect::<BTreeSet<_>>(),
BTreeSet::from([
@@ -82,6 +83,49 @@ fn manifest_has_final_identity_features_and_radroots_dependencies() {
"radroots_signing",
])
);
+
+ for forbidden in [
+ "keyring",
+ "nostr-sdk",
+ "nostr_sdk",
+ "radroots_outbox",
+ "radroots_storage",
+ "radroots_transport",
+ "reqwest",
+ "sqlx",
+ "tokio",
+ ] {
+ assert!(
+ !dependencies.contains(forbidden),
+ "portable Nostr adapter must not depend on `{forbidden}`"
+ );
+ }
+
+ let features = table_keys(MANIFEST, "[features]");
+ for required in ["default", "std", "events", "signing", "nip17", "blossom"] {
+ assert!(
+ features.contains(required),
+ "manifest is missing supported feature `{required}`"
+ );
+ }
+ for feature in features {
+ let declaration = table_value(MANIFEST, "[features]", feature)
+ .unwrap_or_else(|| panic!("missing feature declaration `{feature}`"));
+ for forbidden in [
+ "client",
+ "http",
+ "network",
+ "relay",
+ "reqwest",
+ "runtime",
+ "transport",
+ ] {
+ assert!(
+ !declaration.contains(forbidden),
+ "feature `{feature}` activates forbidden live-I/O authority `{forbidden}`"
+ );
+ }
+ }
}
#[test]
@@ -396,6 +440,46 @@ fn focused_nip_and_blossom_features_own_no_network_operations() {
}
}
+#[test]
+fn production_api_declares_no_traits_or_host_runtime_implementations() {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let mut public_traits = BTreeSet::new();
+
+ for source_path in rust_sources(&manifest_dir.join("src")) {
+ let source = fs::read_to_string(&source_path)
+ .unwrap_or_else(|error| panic!("failed to read {}: {error}", source_path.display()));
+ let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source);
+ for line in production.lines() {
+ let trimmed = line.trim_start();
+ if let Some(name) = trimmed
+ .strip_prefix("pub trait ")
+ .and_then(|rest| rest.split([':', '<', ' ']).next())
+ {
+ public_traits.insert(name.to_owned());
+ }
+ for forbidden in [
+ "nostr_sdk::",
+ "reqwest::",
+ "sqlx::",
+ "tokio::spawn",
+ "std::net::",
+ "std::thread::spawn",
+ ] {
+ assert!(
+ !trimmed.contains(forbidden),
+ "{} owns forbidden host/runtime implementation `{forbidden}`: {trimmed}",
+ source_path.display()
+ );
+ }
+ }
+ }
+
+ assert!(
+ public_traits.is_empty(),
+ "concrete protocol adapter must not publish an SPI trait: {public_traits:?}"
+ );
+}
+
fn rust_sources(root: &Path) -> Vec<PathBuf> {
let mut pending = vec![root.to_path_buf()];
let mut sources = Vec::new();