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:
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());
+ }
+ }
+}