lib

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

package_boundary.rs (21039B)


      1 use std::collections::BTreeSet;
      2 use std::fs;
      3 use std::path::{Path, PathBuf};
      4 
      5 #[allow(unused_imports)]
      6 use radroots_nostr::{
      7     Error as _,
      8     event::{Coordinate as _, Event as _, EventId as _, Kind as _, Metadata as _, Timestamp as _},
      9     filter::Filter as _,
     10     key as _,
     11     tag::{Tag as _, TagKind as _, TagStandard as _},
     12 };
     13 
     14 const MANIFEST: &str = include_str!("../Cargo.toml");
     15 const ROOT: &str = include_str!("../src/lib.rs");
     16 const EVENT_MODULE: &str = include_str!("../src/event.rs");
     17 const FILTER_MODULE: &str = include_str!("../src/filter.rs");
     18 const KEY_MODULE: &str = include_str!("../src/key.rs");
     19 const NIP17_MODULE: &str = include_str!("../src/nip17.rs");
     20 const BLOSSOM_MODULE: &str = include_str!("../src/blossom.rs");
     21 const README: &str = include_str!("../README.md");
     22 const SIGNING_MODULE: &str = include_str!("../src/signing.rs");
     23 const TAG_MODULE: &str = include_str!("../src/tag.rs");
     24 const TYPES_MODULE: &str = include_str!("../src/types.rs");
     25 const PUBLIC_API: &str = include_str!("../../../contracts/api_baselines/radroots_nostr.txt");
     26 const IDENTITY_MANIFEST: &str = include_str!("../../identity/Cargo.toml");
     27 const IDENTITY_KEY_MODULE: &str = include_str!("../../identity/src/key.rs");
     28 const TRANSPORT_MANIFEST: &str = include_str!("../../transport_nostr/Cargo.toml");
     29 const TRANSPORT_ROOT: &str = include_str!("../../transport_nostr/src/lib.rs");
     30 
     31 #[test]
     32 fn manifest_has_final_identity_features_and_radroots_dependencies() {
     33     for required in [
     34         "name = \"radroots_nostr\"",
     35         "version = \"0.1.0-alpha\"",
     36         "publish = [\"crates-io\"]",
     37         "[lib]\nname = \"radroots_nostr\"",
     38         "default = [\"std\", \"events\"]",
     39         "radroots_identity = { workspace = true, default-features = false }",
     40         "nostr = { workspace = true, default-features = false, features = [",
     41         "\"nostr/std\"",
     42         "\"radroots_event/std\"",
     43         "\"radroots_event_codec/std\"",
     44         "\"radroots_identity/std\"",
     45     ] {
     46         assert!(
     47             MANIFEST.contains(required),
     48             "manifest is missing `{required}`"
     49         );
     50     }
     51 
     52     let dependencies = table_keys(MANIFEST, "[dependencies]");
     53     for required in [
     54         "radroots_identity",
     55         "radroots_event",
     56         "radroots_event_codec",
     57     ] {
     58         let declaration = table_value(MANIFEST, "[dependencies]", required)
     59             .unwrap_or_else(|| panic!("missing required dependency `{required}`"));
     60         assert!(
     61             !declaration.contains("optional = true"),
     62             "`{required}` must be required"
     63         );
     64     }
     65     for optional in ["radroots_signing", "radroots_blossom"] {
     66         let declaration = table_value(MANIFEST, "[dependencies]", optional)
     67             .unwrap_or_else(|| panic!("missing optional dependency `{optional}`"));
     68         assert!(
     69             declaration.contains("optional = true"),
     70             "`{optional}` must be optional"
     71         );
     72     }
     73     assert_eq!(
     74         dependencies
     75             .iter()
     76             .copied()
     77             .filter(|dependency| dependency.starts_with("radroots_"))
     78             .collect::<BTreeSet<_>>(),
     79         BTreeSet::from([
     80             "radroots_blossom",
     81             "radroots_event",
     82             "radroots_event_codec",
     83             "radroots_identity",
     84             "radroots_signing",
     85         ])
     86     );
     87 
     88     for forbidden in [
     89         "keyring",
     90         "nostr-sdk",
     91         "nostr_sdk",
     92         "radroots_outbox",
     93         "radroots_storage",
     94         "radroots_transport",
     95         "reqwest",
     96         "sqlx",
     97         "tokio",
     98     ] {
     99         assert!(
    100             !dependencies.contains(forbidden),
    101             "portable Nostr adapter must not depend on `{forbidden}`"
    102         );
    103     }
    104 
    105     let features = table_keys(MANIFEST, "[features]");
    106     assert_eq!(
    107         features,
    108         BTreeSet::from(["blossom", "default", "events", "nip17", "signing", "std"]),
    109         "manifest feature vocabulary must match the Release V1 charter"
    110     );
    111     for feature in features {
    112         let declaration = table_value(MANIFEST, "[features]", feature)
    113             .unwrap_or_else(|| panic!("missing feature declaration `{feature}`"));
    114         for forbidden in [
    115             "client",
    116             "http",
    117             "network",
    118             "relay",
    119             "reqwest",
    120             "runtime",
    121             "transport",
    122         ] {
    123             assert!(
    124                 !declaration.contains(forbidden),
    125                 "feature `{feature}` activates forbidden live-I/O authority `{forbidden}`"
    126             );
    127         }
    128     }
    129 }
    130 
    131 #[test]
    132 fn crate_root_establishes_the_final_public_module_skeleton() {
    133     for module in [
    134         "blossom", "event", "filter", "key", "nip17", "signing", "tag",
    135     ] {
    136         assert!(
    137             ROOT.contains(&format!("pub mod {module};")),
    138             "crate root is missing final module `{module}`"
    139         );
    140     }
    141     let root_reexports = ROOT
    142         .lines()
    143         .map(str::trim)
    144         .filter(|line| line.starts_with("pub use "))
    145         .collect::<Vec<_>>();
    146     assert_eq!(
    147         root_reexports,
    148         ["pub use error::Error;"],
    149         "the crate root must re-export only the canonical Error alias"
    150     );
    151     assert!(
    152         !ROOT.contains("pub mod prelude"),
    153         "lower-level Nostr crate must not publish a broad prelude"
    154     );
    155     for retired in [
    156         "codec_adapters",
    157         "draft_signing",
    158         "error",
    159         "event_adapters",
    160         "event_verify",
    161         "events",
    162         "job_adapter",
    163         "types",
    164         "util",
    165     ] {
    166         assert!(
    167             !ROOT.contains(&format!("pub mod {retired};")),
    168             "superseded module remains public: `{retired}`"
    169         );
    170     }
    171 }
    172 
    173 #[test]
    174 fn protocol_values_are_exposed_only_at_explicit_adapter_modules() {
    175     for (module, aliases) in [
    176         (
    177             EVENT_MODULE,
    178             [
    179                 "pub type Coordinate",
    180                 "pub type Event",
    181                 "pub type EventId",
    182                 "pub type Kind",
    183                 "pub type Metadata",
    184                 "pub type Timestamp",
    185             ]
    186             .as_slice(),
    187         ),
    188         (FILTER_MODULE, ["pub type Filter"].as_slice()),
    189         (
    190             TAG_MODULE,
    191             ["pub type Tag", "pub type TagKind", "pub type TagStandard"].as_slice(),
    192         ),
    193     ] {
    194         for alias in aliases {
    195             assert!(
    196                 module.contains(alias),
    197                 "explicit adapter module is missing `{alias}`"
    198             );
    199         }
    200     }
    201 
    202     for forbidden in [
    203         "pub type RadrootsNostrCoordinate",
    204         "pub type RadrootsNostrEvent",
    205         "pub type RadrootsNostrEventId",
    206         "pub type RadrootsNostrFilter",
    207         "pub type RadrootsNostrKind",
    208         "pub type RadrootsNostrMetadata",
    209         "pub type RadrootsNostrTag",
    210         "pub type RadrootsNostrTagKind",
    211         "pub type RadrootsNostrTagStandard",
    212         "pub type RadrootsNostrTimestamp",
    213     ] {
    214         assert!(
    215             !TYPES_MODULE.contains(forbidden),
    216             "broad predecessor alias remains public in types: `{forbidden}`"
    217         );
    218     }
    219 
    220     assert!(ROOT.contains("mod event_convert;"));
    221     assert!(ROOT.contains("mod tags;"));
    222     assert!(!ROOT.contains("pub mod event_convert;"));
    223     assert!(!ROOT.contains("pub mod tags;"));
    224 }
    225 
    226 #[test]
    227 fn live_client_and_http_ownership_belongs_to_transport_nostr() {
    228     for forbidden in [
    229         "nostr-sdk",
    230         "reqwest",
    231         "client =",
    232         "http =",
    233         "radroots_nostr/client",
    234     ] {
    235         assert!(
    236             !MANIFEST.contains(forbidden),
    237             "portable manifest still owns forbidden live-client surface `{forbidden}`"
    238         );
    239     }
    240 
    241     for forbidden in ["pub mod client;", "pub mod relays;", "pub mod nip11;"] {
    242         assert!(
    243             !ROOT.contains(forbidden),
    244             "portable crate root still exposes live-client module `{forbidden}`"
    245         );
    246     }
    247 
    248     for required in ["nostr-sdk", "radroots_transport", "radroots_event_codec"] {
    249         assert!(
    250             TRANSPORT_MANIFEST.contains(required),
    251             "transport manifest is missing live-client ownership marker `{required}`"
    252         );
    253     }
    254     for required in [
    255         "mod auth;",
    256         "mod client;",
    257         "mod relay;",
    258         "mod sink;",
    259         "mod source;",
    260         "mod status;",
    261         "pub use client::{Config, NostrTransport, ReconnectBackoff};",
    262         "pub use relay::{RelayUrl, RelayUrlPolicy};",
    263     ] {
    264         assert!(
    265             TRANSPORT_ROOT.contains(required),
    266             "transport crate root is missing live-client owner `{required}`"
    267         );
    268     }
    269 
    270     let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    271     assert!(!manifest_dir.join("src/client.rs").exists());
    272     assert!(!manifest_dir.join("src/relays.rs").exists());
    273     assert!(!manifest_dir.join("src/nip11.rs").exists());
    274     assert!(
    275         manifest_dir
    276             .join("../transport_nostr/src/client.rs")
    277             .exists()
    278     );
    279     assert!(
    280         !manifest_dir
    281             .join("../transport_nostr/src/relays.rs")
    282             .exists()
    283     );
    284     assert!(
    285         !manifest_dir
    286             .join("../transport_nostr/src/nip11.rs")
    287             .exists()
    288     );
    289     assert!(!TRANSPORT_MANIFEST.contains("reqwest"));
    290 
    291     let forbidden_source = [
    292         "nostr_sdk",
    293         "reqwest::",
    294         "feature = \"client\"",
    295         "feature = \"http\"",
    296         "crate::client",
    297     ];
    298     for source_path in rust_sources(&manifest_dir.join("src")) {
    299         let source = fs::read_to_string(&source_path)
    300             .unwrap_or_else(|error| panic!("failed to read {}: {error}", source_path.display()));
    301         for forbidden in forbidden_source {
    302             assert!(
    303                 !source.contains(forbidden),
    304                 "{} still contains live-client ownership marker `{forbidden}`",
    305                 source_path.display()
    306             );
    307         }
    308     }
    309 }
    310 
    311 #[test]
    312 fn nostr_key_conversion_is_explicit_and_identity_remains_public_only() {
    313     for required in [
    314         "nostr/nip49",
    315         "pub fn public_key_to_nostr",
    316         "pub fn public_key_from_nostr",
    317         "pub fn public_key_to_npub",
    318         "pub fn public_key_from_npub",
    319         "pub fn parse_public_key",
    320         "pub fn parse_secret_key",
    321         "pub fn secret_key_to_nsec",
    322         "pub fn encrypt_secret_key_nip49",
    323         "pub fn encrypt_secret_key_nip49_with_options",
    324         "pub fn decrypt_secret_key_nip49",
    325     ] {
    326         let authority = if required == "nostr/nip49" {
    327             MANIFEST
    328         } else {
    329             KEY_MODULE
    330         };
    331         assert!(
    332             authority.contains(required),
    333             "Nostr key authority is missing `{required}`"
    334         );
    335     }
    336 
    337     for forbidden in ["nostr =", "nip49", "nsec", "ncryptsec", "SecretKey"] {
    338         assert!(
    339             !IDENTITY_MANIFEST.contains(forbidden),
    340             "identity manifest regained Nostr secret ownership `{forbidden}`"
    341         );
    342         assert!(
    343             !IDENTITY_KEY_MODULE.contains(forbidden),
    344             "identity key module regained Nostr secret ownership `{forbidden}`"
    345         );
    346     }
    347 
    348     for secret_function in [
    349         "parse_secret_key",
    350         "secret_key_to_nsec",
    351         "encrypt_secret_key_nip49",
    352         "encrypt_secret_key_nip49_with_options",
    353         "decrypt_secret_key_nip49",
    354     ] {
    355         let signature = format!("pub fn {secret_function}");
    356         let position = KEY_MODULE
    357             .find(&signature)
    358             .unwrap_or_else(|| panic!("missing secret adapter `{secret_function}`"));
    359         let prefix = &KEY_MODULE[..position];
    360         let nearby = &prefix[prefix.len().saturating_sub(160)..];
    361         assert!(
    362             nearby.contains("#[cfg(feature = \"signing\")]"),
    363             "secret adapter `{secret_function}` is not signing-gated"
    364         );
    365     }
    366 }
    367 
    368 #[test]
    369 fn local_signer_consumes_only_the_opaque_secret_boundary() {
    370     for required in [
    371         "pub fn new(secret_key: SecretKey)",
    372         "pub fn generate()",
    373         "pub const fn public_key(&self) -> PublicKey",
    374         "key::SecretKey",
    375     ] {
    376         assert!(
    377             SIGNING_MODULE.contains(required),
    378             "local signer boundary is missing `{required}`"
    379         );
    380     }
    381     for forbidden in [
    382         "pub const fn new(keys: RadrootsNostrKeys)",
    383         "pub fn new(keys: RadrootsNostrKeys)",
    384         "pub fn keys(",
    385         "pub fn secret_key(",
    386     ] {
    387         assert!(
    388             !SIGNING_MODULE.contains(forbidden),
    389             "local signer leaks an upstream representation: `{forbidden}`"
    390         );
    391     }
    392 }
    393 
    394 #[test]
    395 fn focused_nip_and_blossom_features_own_no_network_operations() {
    396     for required in [
    397         "blossom = [\"events\", \"dep:base64\", \"dep:radroots_blossom\"]",
    398         "nip17 = [\"events\", \"nostr/nip44\", \"nostr/nip59\"]",
    399     ] {
    400         assert!(
    401             MANIFEST.contains(required),
    402             "focused feature contract is missing `{required}`"
    403         );
    404     }
    405     for required in [
    406         "pub async fn wrap_message<T>(",
    407         "pub async fn wrap_message_file<T>(",
    408         "pub async fn unwrap_gift_wrap<T>(",
    409         "pub const fn code(&self) -> &'static str",
    410     ] {
    411         assert!(
    412             NIP17_MODULE.contains(required),
    413             "NIP-17 adapter is missing `{required}`"
    414         );
    415     }
    416     for required in [
    417         "pub fn sign_authorization(",
    418         "pub fn encode_authorization_header(",
    419         "pub fn decode_verify_authorization_header(",
    420     ] {
    421         assert!(
    422             BLOSSOM_MODULE.contains(required),
    423             "Blossom adapter is missing `{required}`"
    424         );
    425     }
    426     for (name, source) in [("NIP-17", NIP17_MODULE), ("Blossom", BLOSSOM_MODULE)] {
    427         for forbidden in [
    428             "nostr_sdk",
    429             "reqwest::",
    430             "tokio::spawn",
    431             "std::net",
    432             "TcpStream",
    433             "UdpSocket",
    434             "RelayPool",
    435         ] {
    436             assert!(
    437                 !source.contains(forbidden),
    438                 "{name} adapter owns forbidden network operation `{forbidden}`"
    439             );
    440         }
    441     }
    442     let readme_words = README.split_whitespace().collect::<Vec<_>>().join(" ");
    443     for required in [
    444         "This crate owns no relay client",
    445         "It does not select relays, deliver events, retry operations, or persist message state.",
    446     ] {
    447         assert!(
    448             readme_words.contains(required),
    449             "README is missing focused ownership statement `{required}`"
    450         );
    451     }
    452     for forbidden in [
    453         "Portable relay-client lifecycle",
    454         "With the `client` feature",
    455     ] {
    456         assert!(
    457             !README.contains(forbidden),
    458             "README retains removed network ownership statement `{forbidden}`"
    459         );
    460     }
    461 }
    462 
    463 #[test]
    464 fn production_api_declares_no_traits_or_host_runtime_implementations() {
    465     let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    466     let mut public_traits = BTreeSet::new();
    467 
    468     for source_path in rust_sources(&manifest_dir.join("src")) {
    469         let source = fs::read_to_string(&source_path)
    470             .unwrap_or_else(|error| panic!("failed to read {}: {error}", source_path.display()));
    471         let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source);
    472         for line in production.lines() {
    473             let trimmed = line.trim_start();
    474             if let Some(name) = trimmed
    475                 .strip_prefix("pub trait ")
    476                 .and_then(|rest| rest.split([':', '<', ' ']).next())
    477             {
    478                 public_traits.insert(name.to_owned());
    479             }
    480             for forbidden in [
    481                 "nostr_sdk::",
    482                 "reqwest::",
    483                 "sqlx::",
    484                 "tokio::spawn",
    485                 "std::net::",
    486                 "std::thread::spawn",
    487             ] {
    488                 assert!(
    489                     !trimmed.contains(forbidden),
    490                     "{} owns forbidden host/runtime implementation `{forbidden}`: {trimmed}",
    491                     source_path.display()
    492                 );
    493             }
    494         }
    495     }
    496 
    497     assert!(
    498         public_traits.is_empty(),
    499         "concrete protocol adapter must not publish an SPI trait: {public_traits:?}"
    500     );
    501 }
    502 
    503 #[test]
    504 fn workspace_consumers_do_not_use_superseded_nostr_alias_paths() {
    505     let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    506     let crates_dir = manifest_dir
    507         .parent()
    508         .expect("Nostr crate must have a crates directory")
    509         .to_path_buf();
    510 
    511     for source_path in rust_sources(&crates_dir) {
    512         if source_path.starts_with(&manifest_dir) {
    513             continue;
    514         }
    515         let source = fs::read_to_string(&source_path)
    516             .unwrap_or_else(|error| panic!("failed to read {}: {error}", source_path.display()));
    517         for forbidden in [
    518             "radroots_nostr::codec_adapters::",
    519             "radroots_nostr::draft_signing::",
    520             "radroots_nostr::error::",
    521             "radroots_nostr::event_adapters::",
    522             "radroots_nostr::event_verify::",
    523             "radroots_nostr::events::",
    524             "radroots_nostr::job_adapter::",
    525             "radroots_nostr::prelude",
    526             "radroots_nostr::types::",
    527             "radroots_nostr::util::",
    528         ] {
    529             assert!(
    530                 !source.contains(forbidden),
    531                 "{} still imports superseded Nostr path `{forbidden}`",
    532                 source_path.display()
    533             );
    534         }
    535     }
    536 }
    537 
    538 #[test]
    539 fn superseded_surface_retirement_is_explicit_and_release_bounded() {
    540     assert!(!MANIFEST.contains("codec = []"));
    541     for forbidden in ["pub fn radroots_nostr_", "pub async fn radroots_nostr_"] {
    542         for source_path in rust_sources(&PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src")) {
    543             let source = fs::read_to_string(&source_path).unwrap_or_else(|error| {
    544                 panic!("failed to read {}: {error}", source_path.display())
    545             });
    546             assert!(
    547                 !source.contains(forbidden),
    548                 "{} retains prefixed public item `{forbidden}`",
    549                 source_path.display()
    550             );
    551         }
    552     }
    553     assert!(MANIFEST.contains("publish = [\"crates-io\"]"));
    554     assert!(
    555         !Path::new(env!("CARGO_MANIFEST_DIR"))
    556             .join("COMPATIBILITY.md")
    557             .exists(),
    558         "the completed migration record must not ship as an active compatibility contract"
    559     );
    560 
    561     let public_modules = PUBLIC_API
    562         .lines()
    563         .filter_map(|line| line.strip_prefix("pub mod radroots_nostr::"))
    564         .collect::<BTreeSet<_>>();
    565     assert_eq!(
    566         public_modules,
    567         BTreeSet::from([
    568             "blossom", "event", "filter", "key", "nip17", "signing", "tag"
    569         ]),
    570         "reviewed API baseline must expose only chartered modules"
    571     );
    572     for forbidden in [
    573         "::RadrootsNostr",
    574         "::radroots_nostr_",
    575         "pub radroots_nostr::event::ApplicationHandlerSpec::",
    576         "pub radroots_nostr::nip17::WrapOptions::",
    577     ] {
    578         assert!(
    579             !PUBLIC_API.contains(forbidden),
    580             "reviewed API baseline retains forbidden surface `{forbidden}`"
    581         );
    582     }
    583     for required in [
    584         "pub fn radroots_nostr::blossom::SignedAuthorization::from_signed_event",
    585         "pub fn radroots_nostr::blossom::encode_signed_event_authorization_header",
    586     ] {
    587         assert!(
    588             PUBLIC_API.contains(required),
    589             "reviewed API baseline is missing `{required}`"
    590         );
    591     }
    592 }
    593 
    594 fn rust_sources(root: &Path) -> Vec<PathBuf> {
    595     let mut pending = vec![root.to_path_buf()];
    596     let mut sources = Vec::new();
    597     while let Some(directory) = pending.pop() {
    598         for entry in fs::read_dir(&directory)
    599             .unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display()))
    600         {
    601             let path = entry
    602                 .expect("source directory entry must be readable")
    603                 .path();
    604             if path.is_dir() {
    605                 pending.push(path);
    606             } else if path.extension().is_some_and(|extension| extension == "rs") {
    607                 sources.push(path);
    608             }
    609         }
    610     }
    611     sources
    612 }
    613 
    614 fn table_keys<'a>(source: &'a str, header: &str) -> BTreeSet<&'a str> {
    615     table_lines(source, header)
    616         .filter_map(|line| line.split_once('=').map(|(key, _)| key.trim()))
    617         .collect()
    618 }
    619 
    620 fn table_value<'a>(source: &'a str, header: &str, key: &str) -> Option<&'a str> {
    621     table_lines(source, header).find_map(|line| {
    622         let (candidate, value) = line.split_once('=')?;
    623         (candidate.trim() == key).then_some(value.trim())
    624     })
    625 }
    626 
    627 fn table_lines<'a>(source: &'a str, header: &str) -> impl Iterator<Item = &'a str> {
    628     let start = source
    629         .find(header)
    630         .unwrap_or_else(|| panic!("missing table `{header}`"));
    631     source[start + header.len()..]
    632         .lines()
    633         .skip_while(|line| line.trim().is_empty())
    634         .take_while(|line| !line.trim_start().starts_with('['))
    635         .filter(|line| !line.trim().is_empty() && !line.trim_start().starts_with('#'))
    636 }