lib

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

commit fbb51079d6a34617b2627805dbc9e69a137dbdfc
parent c3f57d348246fbf83a56af25eab8bda6cd87fcfa
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 14:06:08 +0000

features: govern Nostr std-only base

- make the bare protocol surface explicitly std-backed
- reject direct and conditional no-std crate attributes structurally
- document the no-default feature support boundary
- execute the bare library build as a Nix check

Diffstat:
Mbuild/nix/checks.nix | 13+++++++++++++
Mcrates/nostr/README | 8++++++++
Mcrates/nostr/src/lib.rs | 2+-
Mcrates/outbox/contracts/migration_authority_v1.manifest.json | 8++++----
Mcrates/outbox/contracts/migration_authority_v1.manifest.sha256 | 2+-
Mcrates/outbox/contracts/phase1_publication_v1.manifest.json | 4++--
Mcrates/outbox/contracts/phase1_publication_v1.manifest.sha256 | 2+-
Mtools/xtask/src/contract.rs | 2++
Atools/xtask/src/contract/feature_support.rs | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 136 insertions(+), 9 deletions(-)

diff --git a/build/nix/checks.nix b/build/nix/checks.nix @@ -40,6 +40,18 @@ let installPhaseCommand = "mkdir -p $out"; } ); + nostrBareCheck = common.craneLib.mkCargoDerivation ( + common.commonCraneArgs + // { + inherit (common) cargoArtifacts; + pname = "radroots-nostr-bare-check"; + doCheck = false; + buildPhaseCargoCommand = '' + cargo check -p radroots_nostr --lib --no-default-features + ''; + installPhaseCommand = "mkdir -p $out"; + } + ); outboxFeatureMatrixCheck = common.craneLib.mkCargoDerivation ( common.commonCraneArgs // { @@ -107,6 +119,7 @@ in cargo-check = cargoCheck; cargo-test = cargoTest; blossom-no-default-check = blossomNoDefaultCheck; + nostr-bare-check = nostrBareCheck; blossom-raster-decode-test = blossomRasterDecodeTest; outbox-feature-matrix = outboxFeatureMatrixCheck; blossom-decoder-fuzz-smoke = common.mkRepoCheck { diff --git a/crates/nostr/README b/crates/nostr/README @@ -14,6 +14,14 @@ primitives for the `radroots` core libraries. feature; * optional NIP-11 and NIP-17 support across feature-gated builds. +## Runtime and feature support + +`radroots_nostr` is a standard-library crate. Disabling default Cargo features +selects its smallest protocol surface; it does not select a `no_std` runtime. +The bare `--no-default-features` library build is supported and checked, while +feature combinations not listed by the governed support lanes carry no +compatibility claim. + The `blossom` feature signs kind-24242 authorization events and encodes or verifies their `Authorization: Nostr` HTTP values. It does not publish these ephemeral authorization events to relays. Pure BUD-11 claim parsing and policy diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![forbid(unsafe_code)] extern crate alloc; diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json @@ -305,10 +305,10 @@ }, { "file": { - "byte_length": 494829, + "byte_length": 494914, "hash_algorithm": "sha256_bytes_v1", "path": "tools/xtask/src/contract.rs", - "sha256": "487946ba039f8d72b5ed1d372c42d49337d009d161aa31495fc7b232da4c03e8" + "sha256": "d01a9e90f179f6bd4bd01edfb0e8fc87e9a92fa555f9ea53364518cac286863a" }, "role": "contract_dispatch" }, @@ -332,10 +332,10 @@ }, { "file": { - "byte_length": 4588, + "byte_length": 4987, "hash_algorithm": "sha256_bytes_v1", "path": "build/nix/checks.nix", - "sha256": "830e46d81576372b3a7dd63c911948051defb6327db8212be8d134404544ef5e" + "sha256": "8fb9d256a234a91c2f863594aa6962dcc458bce82d499dd6a993811b11182e2c" }, "role": "nix_feature_check" }, diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256 @@ -1 +1 @@ -360da1cc5c0924b721c10e5e91e803b8b65430b32c3af0f3bd02ee1f9d9d3267 +398598a40de3a85688076383c9f1971fbcd26a2f99d07a9c9001ed50e2f5038e diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.json b/crates/outbox/contracts/phase1_publication_v1.manifest.json @@ -112,9 +112,9 @@ }, { "file": { - "byte_length": 494829, + "byte_length": 494914, "path": "tools/xtask/src/contract.rs", - "sha256": "487946ba039f8d72b5ed1d372c42d49337d009d161aa31495fc7b232da4c03e8" + "sha256": "d01a9e90f179f6bd4bd01edfb0e8fc87e9a92fa555f9ea53364518cac286863a" }, "role": "contract_dispatch" }, diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 b/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 @@ -1 +1 @@ -25055c1dcb12ff7a9d0a83eccae00ddb64549be81006c6962fc7b57cc2e1c954 +51fccb99431ed736313d1513b325ce5daf165aca16342eb314704e9c733659af diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -7,6 +7,7 @@ mod blossom_publication_readiness; mod blossom_raster_decoder_security; mod comment_authority; mod deletion_authority; +mod feature_support; mod food_availability_projection; mod nip09_reconciliation; mod outbox_migration; @@ -87,6 +88,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), String> { + feature_support::validate_feature_support(workspace_root)?; validate_event_contract_registry_v7_inventory(workspace_root)?; validate_nip09_reconciliation_manifest(workspace_root)?; validate_food_availability_projection_manifest(workspace_root)?; diff --git a/tools/xtask/src/contract/feature_support.rs b/tools/xtask/src/contract/feature_support.rs @@ -0,0 +1,104 @@ +use std::fs; +use std::path::Path; +use syn::parse::Parser; +use syn::punctuated::Punctuated; +use syn::{Attribute, Meta, Token}; + +const NOSTR_MANIFEST_RELATIVE: &str = "crates/nostr/Cargo.toml"; +const NOSTR_LIB_RELATIVE: &str = "crates/nostr/src/lib.rs"; + +pub(super) fn validate_feature_support(workspace_root: &Path) -> Result<(), String> { + validate_nostr_manifest(workspace_root)?; + let path = workspace_root.join(NOSTR_LIB_RELATIVE); + let source = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + validate_nostr_std_only_source(&source) +} + +fn validate_nostr_manifest(workspace_root: &Path) -> Result<(), String> { + let path = workspace_root.join(NOSTR_MANIFEST_RELATIVE); + let source = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + let manifest = toml::from_str::<toml::Value>(&source) + .map_err(|error| format!("parse {}: {error}", path.display()))?; + let features = manifest + .get("features") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{NOSTR_MANIFEST_RELATIVE} must define features"))?; + let default = string_array(features.get("default"), "radroots_nostr default feature")?; + let std = string_array(features.get("std"), "radroots_nostr std feature")?; + if default != ["std"] || !std.is_empty() { + return Err( + "radroots_nostr must remain std-only with default = [\"std\"] and an empty std marker" + .to_owned(), + ); + } + Ok(()) +} + +fn string_array<'a>(value: Option<&'a toml::Value>, label: &str) -> Result<Vec<&'a str>, String> { + value + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{label} must be an array"))? + .iter() + .map(|value| { + value + .as_str() + .ok_or_else(|| format!("{label} entries must be strings")) + }) + .collect() +} + +fn validate_nostr_std_only_source(source: &str) -> Result<(), String> { + let file = syn::parse_file(source) + .map_err(|error| format!("parse {NOSTR_LIB_RELATIVE} as Rust: {error}"))?; + for attribute in &file.attrs { + if attribute.path().is_ident("no_std") || cfg_attr_contains_no_std(attribute)? { + return Err(format!( + "{NOSTR_LIB_RELATIVE} must not declare a no_std crate mode" + )); + } + } + Ok(()) +} + +fn cfg_attr_contains_no_std(attribute: &Attribute) -> Result<bool, String> { + if !attribute.path().is_ident("cfg_attr") { + return Ok(false); + } + let Meta::List(list) = &attribute.meta else { + return Err("cfg_attr must use list syntax".to_owned()); + }; + let entries = Punctuated::<Meta, Token![,]>::parse_terminated + .parse2(list.tokens.clone()) + .map_err(|error| format!("parse cfg_attr in {NOSTR_LIB_RELATIVE}: {error}"))?; + Ok(entries.iter().skip(1).any(meta_contains_no_std)) +} + +fn meta_contains_no_std(meta: &Meta) -> bool { + match meta { + Meta::Path(path) => path.is_ident("no_std"), + Meta::List(list) => Punctuated::<Meta, Token![,]>::parse_terminated + .parse2(list.tokens.clone()) + .map(|entries| entries.iter().any(meta_contains_no_std)) + .unwrap_or(false), + Meta::NameValue(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn std_only_source_rejects_direct_and_conditional_no_std_modes() { + validate_nostr_std_only_source("#![forbid(unsafe_code)]\nextern crate alloc;") + .expect("std-only source"); + for invalid in [ + "#![no_std]\nextern crate alloc;", + "#![cfg_attr(not(feature = \"std\"), no_std)]\nextern crate alloc;", + ] { + assert!(validate_nostr_std_only_source(invalid).is_err()); + } + } +}