lib

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

commit b943892c0c085e839d289c000b7b8be1907bc2aa
parent 7f5b979d04b65bd1847b0cc2c317a2daa9273127
Author: triesap <tyson@radroots.org>
Date:   Fri, 31 Jul 2026 09:51:27 +0000

nostr: move Nostr key and NIP-19 conversion out of identity

- Convert canonical identity public keys explicitly to and from Nostr and npub.
- Add opaque nsec and NIP-49 adapters behind the signing feature.
- Redact secret values, passwords, ciphertexts, and normalized error reports.
- Retire legacy key helpers and enforce identity and package ownership.

Diffstat:
Mcrates/nostr/Cargo.toml | 7++++++-
Mcrates/nostr/src/error.rs | 22++++++++++++++++++++++
Mcrates/nostr/src/key.rs | 333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/nostr/src/lib.rs | 16++++++----------
Dcrates/nostr/src/parse.rs | 23-----------------------
Mcrates/nostr/src/util.rs | 9+--------
Mcrates/nostr/tests/coverage.rs | 22+++++++++-------------
Mcrates/nostr/tests/package_boundary.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 437 insertions(+), 55 deletions(-)

diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml @@ -34,7 +34,12 @@ events = [ "radroots_event_codec/json", ] nip17 = ["std", "codec", "nostr/nip44", "nostr/nip59"] -signing = ["events", "dep:radroots_signing", "radroots_signing/std"] +signing = [ + "events", + "dep:radroots_signing", + "nostr/nip49", + "radroots_signing/std", +] [dependencies] base64 = { workspace = true, optional = true } diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs @@ -3,6 +3,28 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum RadrootsNostrError { + #[error("invalid Nostr public key")] + InvalidPublicKey, + + #[error("invalid NIP-19 npub public key")] + InvalidNpub, + + #[cfg(feature = "signing")] + #[error("invalid Nostr secret key")] + InvalidSecretKey, + + #[cfg(feature = "signing")] + #[error("invalid NIP-49 encrypted secret key")] + InvalidEncryptedSecretKey, + + #[cfg(feature = "signing")] + #[error("NIP-49 secret-key encryption failed")] + SecretKeyEncryption, + + #[cfg(feature = "signing")] + #[error("NIP-49 secret-key decryption failed")] + SecretKeyDecryption, + #[error("Nostr event kind {kind} exceeds {max}")] KindOutOfRange { kind: u32, max: u16 }, diff --git a/crates/nostr/src/key.rs b/crates/nostr/src/key.rs @@ -2,3 +2,336 @@ //! //! Step 125 moves the existing conversion behavior to this durable public //! module without returning Nostr representation policy to identity. + +use alloc::string::String; + +use nostr::nips::nip19::{FromBech32, ToBech32}; +use radroots_identity::PublicKey; + +use crate::Error; + +/// Converts a canonical Radroots public key into its Nostr representation. +pub fn public_key_to_nostr(public_key: PublicKey) -> Result<nostr::PublicKey, Error> { + nostr::PublicKey::from_slice(public_key.as_bytes()).map_err(|_| Error::InvalidPublicKey) +} + +/// Converts a Nostr public key into the canonical Radroots representation. +pub fn public_key_from_nostr(public_key: nostr::PublicKey) -> Result<PublicKey, Error> { + PublicKey::from_bytes(public_key.to_bytes()).map_err(|_| Error::InvalidPublicKey) +} + +/// Encodes a canonical Radroots public key as a NIP-19 `npub`. +pub fn public_key_to_npub(public_key: PublicKey) -> Result<String, Error> { + let public_key = public_key_to_nostr(public_key)?; + match public_key.to_bech32() { + Ok(encoded) => Ok(encoded), + Err(error) => match error {}, + } +} + +/// Decodes a NIP-19 `npub` into the canonical Radroots public-key value. +pub fn public_key_from_npub(encoded: &str) -> Result<PublicKey, Error> { + nostr::PublicKey::from_bech32(encoded) + .map_err(|_| Error::InvalidNpub) + .and_then(public_key_from_nostr) +} + +/// Parses a canonical hexadecimal public key or a NIP-19 `npub`. +pub fn parse_public_key(encoded: &str) -> Result<PublicKey, Error> { + if encoded.starts_with("npub1") { + public_key_from_npub(encoded) + } else { + PublicKey::from_hex(encoded).map_err(|_| Error::InvalidPublicKey) + } +} + +/// An opaque local Nostr secret key. +/// +/// The value does not implement `Clone`, serialization, or unrestricted +/// plaintext access. Debug output is always redacted; the concrete local +/// signing adapter consumes the value through crate-private integration. +/// +/// ```compile_fail +/// use radroots_nostr::key::SecretKey; +/// +/// let key = SecretKey::parse( +/// "0000000000000000000000000000000000000000000000000000000000000001", +/// )?; +/// let _duplicate = key.clone(); +/// # Ok::<(), radroots_nostr::Error>(()) +/// ``` +#[cfg(feature = "signing")] +pub struct SecretKey { + inner: nostr::SecretKey, +} + +#[cfg(feature = "signing")] +impl SecretKey { + /// Parses exact hexadecimal or NIP-19 `nsec` text. + /// + /// Errors never retain or render the supplied secret material. + pub fn parse(encoded: &str) -> Result<Self, Error> { + nostr::SecretKey::parse(encoded) + .map(|inner| Self { inner }) + .map_err(|_| Error::InvalidSecretKey) + } + + /// Derives the canonical public identity without exposing secret bytes. + pub fn public_key(&self) -> Result<PublicKey, Error> { + let public_key = nostr::Keys::new(self.inner.clone()).public_key(); + public_key_from_nostr(public_key) + } +} + +#[cfg(feature = "signing")] +impl core::fmt::Debug for SecretKey { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_tuple("SecretKey") + .field(&"[redacted]") + .finish() + } +} + +/// NIP-49 metadata describing how the plaintext key was previously handled. +#[cfg(feature = "signing")] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum Nip49KeySecurity { + /// The plaintext key is known to have been handled insecurely. + Weak, + /// The plaintext key is not known to have been handled insecurely. + Medium, + /// The caller does not track plaintext-key handling. + #[default] + Unknown, +} + +#[cfg(feature = "signing")] +impl From<Nip49KeySecurity> for nostr::nips::nip49::KeySecurity { + fn from(value: Nip49KeySecurity) -> Self { + match value { + Nip49KeySecurity::Weak => Self::Weak, + Nip49KeySecurity::Medium => Self::Medium, + Nip49KeySecurity::Unknown => Self::Unknown, + } + } +} + +/// Explicit NIP-49 encryption parameters. +#[cfg(feature = "signing")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Nip49Options { + log_n: u8, + key_security: Nip49KeySecurity, +} + +#[cfg(feature = "signing")] +impl Nip49Options { + /// Creates NIP-49 options with an explicit scrypt `log2(N)` work factor. + #[must_use] + pub const fn new(log_n: u8, key_security: Nip49KeySecurity) -> Self { + Self { + log_n, + key_security, + } + } + + /// Returns the scrypt `log2(N)` work factor. + #[must_use] + pub const fn log_n(self) -> u8 { + self.log_n + } + + /// Returns the NIP-49 plaintext-key handling metadata. + #[must_use] + pub const fn key_security(self) -> Nip49KeySecurity { + self.key_security + } +} + +#[cfg(feature = "signing")] +impl Default for Nip49Options { + fn default() -> Self { + Self::new(16, Nip49KeySecurity::Unknown) + } +} + +/// Parses a Nostr secret key from exact hexadecimal or NIP-19 `nsec` text. +/// +/// Errors never retain or render the supplied secret material. +#[cfg(feature = "signing")] +pub fn parse_secret_key(encoded: &str) -> Result<SecretKey, Error> { + SecretKey::parse(encoded) +} + +/// Encodes a Nostr secret key as NIP-19 `nsec` text. +#[cfg(feature = "signing")] +pub fn secret_key_to_nsec(secret_key: &SecretKey) -> String { + match secret_key.inner.to_bech32() { + Ok(encoded) => encoded, + Err(error) => match error {}, + } +} + +/// Encrypts a Nostr secret key into a NIP-49 `ncryptsec` payload. +#[cfg(feature = "signing")] +pub fn encrypt_secret_key_nip49(secret_key: &SecretKey, password: &str) -> Result<String, Error> { + encrypt_secret_key_nip49_with_options(secret_key, password, Nip49Options::default()) +} + +/// Encrypts a Nostr secret key with explicit NIP-49 parameters. +#[cfg(feature = "signing")] +pub fn encrypt_secret_key_nip49_with_options( + secret_key: &SecretKey, + password: &str, + options: Nip49Options, +) -> Result<String, Error> { + let encrypted = nostr::nips::nip49::EncryptedSecretKey::new( + &secret_key.inner, + password, + options.log_n, + options.key_security.into(), + ) + .map_err(|_| Error::SecretKeyEncryption)?; + encrypted + .to_bech32() + .map_err(|_| Error::SecretKeyEncryption) +} + +/// Decrypts a NIP-49 `ncryptsec` payload into a Nostr secret key. +/// +/// Parse, password, and ciphertext failures are deliberately normalized so +/// diagnostics never retain the encrypted payload, password, or plaintext. +#[cfg(feature = "signing")] +pub fn decrypt_secret_key_nip49(encrypted: &str, password: &str) -> Result<SecretKey, Error> { + nostr::nips::nip49::EncryptedSecretKey::from_bech32(encrypted) + .map_err(|_| Error::InvalidEncryptedSecretKey)? + .decrypt(password) + .map(|inner| SecretKey { inner }) + .map_err(|_| Error::SecretKeyDecryption) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::FIXTURE_ALICE; + + #[cfg(feature = "signing")] + const NCRYPTSEC: &str = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p"; + #[cfg(feature = "signing")] + const NCRYPTSEC_SECRET_HEX: &str = + "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683"; + + #[test] + fn native_public_key_round_trips_through_nostr_hex_and_npub() { + let native = + PublicKey::from_hex(FIXTURE_ALICE.public_key_hex).expect("native public key fixture"); + let nostr = public_key_to_nostr(native).expect("Nostr public key"); + + assert_eq!(nostr.to_hex(), FIXTURE_ALICE.public_key_hex); + assert_eq!( + public_key_from_nostr(nostr).expect("native public key"), + native + ); + assert_eq!( + public_key_to_npub(native).expect("npub"), + FIXTURE_ALICE.npub + ); + assert_eq!( + public_key_from_npub(FIXTURE_ALICE.npub).expect("native npub"), + native + ); + assert_eq!( + parse_public_key(FIXTURE_ALICE.public_key_hex).expect("hex public key"), + native + ); + assert_eq!( + parse_public_key(FIXTURE_ALICE.npub).expect("npub public key"), + native + ); + } + + #[test] + fn public_key_parsing_rejects_wrong_nip19_kinds_without_echoing_input() { + let invalid = "nsec1-do-not-disclose-public-key-input"; + let error = parse_public_key(invalid).expect_err("secret HRP is not a public key"); + + assert!(matches!(error, Error::InvalidPublicKey)); + assert!(!error.to_string().contains(invalid)); + assert!(!format!("{error:?}").contains(invalid)); + + let malformed_npub = "npub1-do-not-disclose-public-key-input"; + let error = public_key_from_npub(malformed_npub).expect_err("malformed npub"); + assert!(matches!(error, Error::InvalidNpub)); + assert!(!error.to_string().contains(malformed_npub)); + assert!(!format!("{error:?}").contains(malformed_npub)); + } + + #[cfg(feature = "signing")] + #[test] + fn secret_key_hex_and_nsec_vectors_round_trip() { + let from_hex = parse_secret_key(FIXTURE_ALICE.secret_key_hex).expect("hex secret key"); + let from_nsec = parse_secret_key(FIXTURE_ALICE.nsec).expect("nsec secret key"); + + assert_eq!(secret_key_to_nsec(&from_hex), FIXTURE_ALICE.nsec); + assert_eq!(secret_key_to_nsec(&from_nsec), FIXTURE_ALICE.nsec); + assert_eq!( + from_hex.public_key().expect("public key").to_hex(), + FIXTURE_ALICE.public_key_hex + ); + for rendered in [format!("{from_hex:?}"), format!("{from_nsec:?}")] { + assert!(rendered.contains("[redacted]")); + assert!(!rendered.contains(FIXTURE_ALICE.secret_key_hex)); + assert!(!rendered.contains(FIXTURE_ALICE.nsec)); + } + } + + #[cfg(feature = "signing")] + #[test] + fn nip49_known_vector_decrypts_and_round_trips_with_explicit_options() { + let decrypted = decrypt_secret_key_nip49(NCRYPTSEC, "nostr").expect("known ncryptsec"); + let expected = parse_secret_key(NCRYPTSEC_SECRET_HEX).expect("known secret key"); + assert_eq!( + secret_key_to_nsec(&decrypted), + secret_key_to_nsec(&expected) + ); + + let options = Nip49Options::new(10, Nip49KeySecurity::Medium); + assert_eq!(options.log_n(), 10); + assert_eq!(options.key_security(), Nip49KeySecurity::Medium); + let encrypted = encrypt_secret_key_nip49_with_options(&decrypted, "test-password", options) + .expect("encrypt ncryptsec"); + let round_trip = + decrypt_secret_key_nip49(&encrypted, "test-password").expect("decrypt ncryptsec"); + assert_eq!( + secret_key_to_nsec(&round_trip), + secret_key_to_nsec(&decrypted) + ); + } + + #[cfg(feature = "signing")] + #[test] + fn secret_failures_are_redacted() { + let invalid_secret = "nsec1-do-not-disclose-secret-input"; + let parse_error = parse_secret_key(invalid_secret).expect_err("invalid secret"); + assert!(matches!(parse_error, Error::InvalidSecretKey)); + assert!(!parse_error.to_string().contains(invalid_secret)); + assert!(!format!("{parse_error:?}").contains(invalid_secret)); + + let password = "do-not-disclose-password"; + let decrypt_error = + decrypt_secret_key_nip49(NCRYPTSEC, password).expect_err("wrong password"); + assert!(matches!(decrypt_error, Error::SecretKeyDecryption)); + for rendered in [decrypt_error.to_string(), format!("{decrypt_error:?}")] { + assert!(!rendered.contains(password)); + assert!(!rendered.contains(NCRYPTSEC)); + assert!(!rendered.contains(NCRYPTSEC_SECRET_HEX)); + } + + let encrypted_error = + decrypt_secret_key_nip49(invalid_secret, password).expect_err("invalid ncryptsec"); + assert!(matches!(encrypted_error, Error::InvalidEncryptedSecretKey)); + assert!(!encrypted_error.to_string().contains(invalid_secret)); + assert!(!format!("{encrypted_error:?}").contains(invalid_secret)); + } +} diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs @@ -12,7 +12,6 @@ pub mod event; pub mod events; pub mod filter; pub mod key; -pub mod parse; pub mod tag; pub mod tags; pub mod types; @@ -106,7 +105,12 @@ pub mod prelude { radroots_nostr_metadata_has_fields, }; - pub use crate::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys}; + #[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::types::{ RadrootsNostrCoordinate, RadrootsNostrEvent, RadrootsNostrEventId, @@ -117,14 +121,6 @@ pub mod prelude { RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp, RadrootsNostrToBech32, RadrootsNostrUrl, }; - pub use crate::util::radroots_nostr_npub_string; - - #[cfg(feature = "nip17")] - pub use crate::nip17::{ - RadrootsNip17Error, RadrootsNip17Rumor, RadrootsNip17WrapOptions, - radroots_nostr_unwrap_gift_wrap, radroots_nostr_wrap_message, - radroots_nostr_wrap_message_file, - }; #[cfg(feature = "events")] pub use crate::event_adapters::{to_post_event_metadata, to_profile_event_metadata}; diff --git a/crates/nostr/src/parse.rs b/crates/nostr/src/parse.rs @@ -1,23 +0,0 @@ -use crate::types::{RadrootsNostrFromBech32, RadrootsNostrPublicKey}; -use alloc::{string::String, string::ToString, vec::Vec}; - -#[derive(Debug, thiserror::Error)] -pub enum ParseError { - #[error("invalid pubkey format: {0}")] - Invalid(String), -} - -pub fn radroots_nostr_parse_pubkey(s: &str) -> Result<RadrootsNostrPublicKey, ParseError> { - RadrootsNostrPublicKey::from_bech32(s) - .or_else(|_| RadrootsNostrPublicKey::from_hex(s)) - .map_err(|_| ParseError::Invalid(s.to_string())) -} - -pub fn radroots_nostr_parse_pubkeys( - input: &[String], -) -> Result<Vec<RadrootsNostrPublicKey>, ParseError> { - input - .iter() - .map(|s| radroots_nostr_parse_pubkey(s)) - .collect() -} diff --git a/crates/nostr/src/util.rs b/crates/nostr/src/util.rs @@ -1,11 +1,4 @@ -use crate::types::{ - RadrootsNostrEvent, RadrootsNostrPublicKey, RadrootsNostrTimestamp, RadrootsNostrToBech32, -}; -use alloc::string::String; - -pub fn radroots_nostr_npub_string(pk: &RadrootsNostrPublicKey) -> Option<String> { - pk.to_bech32().ok() -} +use crate::types::{RadrootsNostrEvent, RadrootsNostrTimestamp}; pub fn created_at_u32_saturating(ts: RadrootsNostrTimestamp) -> u32 { u32::try_from(ts.as_secs()).unwrap_or(u32::MAX) diff --git a/crates/nostr/tests/coverage.rs b/crates/nostr/tests/coverage.rs @@ -17,7 +17,7 @@ use radroots_nostr::filter::{ radroots_nostr_filter_kind, radroots_nostr_filter_new_events, radroots_nostr_filter_tag, radroots_nostr_kind, }; -use radroots_nostr::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys}; +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, @@ -29,9 +29,7 @@ use radroots_nostr::types::{ RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrRelayUrl, RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp, }; -use radroots_nostr::util::{ - created_at_u32_saturating, event_created_at_u32_saturating, radroots_nostr_npub_string, -}; +use radroots_nostr::util::{created_at_u32_saturating, event_created_at_u32_saturating}; use test_fixtures::RELAY_PRIMARY_WSS; fn make_keys() -> RadrootsNostrKeys { @@ -189,17 +187,14 @@ fn filter_helpers_cover_all_paths() { fn parse_helpers_cover_success_and_failure() { let keys = make_keys(); let pubkey_hex = keys.public_key().to_hex(); - let ok = radroots_nostr_parse_pubkey(pubkey_hex.as_str()); + let ok = parse_public_key(pubkey_hex.as_str()); assert!(ok.is_ok()); - let invalid = radroots_nostr_parse_pubkey("invalid"); + let invalid = parse_public_key("invalid"); assert!(invalid.is_err()); - let parsed = radroots_nostr_parse_pubkeys(std::slice::from_ref(&pubkey_hex)); - assert!(parsed.is_ok()); - - let parse_err = radroots_nostr_parse_pubkeys(&[pubkey_hex, "invalid".to_string()]); - assert!(parse_err.is_err()); + let npub = public_key_to_npub(ok.expect("public key")).expect("npub"); + assert!(parse_public_key(&npub).is_ok()); } #[test] @@ -392,8 +387,9 @@ fn tag_helpers_cover_matchers_and_resolve_paths() { #[test] fn util_helpers_cover_conversion_paths() { let keys = make_keys(); - let npub = radroots_nostr_npub_string(&keys.public_key()); - assert!(npub.is_some()); + let native = public_key_from_nostr(keys.public_key()).expect("native public key"); + let npub = public_key_to_npub(native).expect("npub"); + assert!(npub.starts_with("npub1")); let max = RadrootsNostrTimestamp::from(u64::from(u32::MAX)); let overflow = RadrootsNostrTimestamp::from(u64::from(u32::MAX) + 1); diff --git a/crates/nostr/tests/package_boundary.rs b/crates/nostr/tests/package_boundary.rs @@ -7,6 +7,9 @@ use radroots_nostr::{Error as _, event as _, filter as _, key as _, tag as _}; const MANIFEST: &str = include_str!("../Cargo.toml"); const ROOT: &str = include_str!("../src/lib.rs"); +const KEY_MODULE: &str = include_str!("../src/key.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"); const TRANSPORT_ROOT: &str = include_str!("../../transport_nostr/src/lib.rs"); @@ -161,6 +164,63 @@ fn live_client_and_http_ownership_belongs_to_transport_nostr() { } } +#[test] +fn nostr_key_conversion_is_explicit_and_identity_remains_public_only() { + for required in [ + "nostr/nip49", + "pub fn public_key_to_nostr", + "pub fn public_key_from_nostr", + "pub fn public_key_to_npub", + "pub fn public_key_from_npub", + "pub fn parse_public_key", + "pub fn parse_secret_key", + "pub fn secret_key_to_nsec", + "pub fn encrypt_secret_key_nip49", + "pub fn encrypt_secret_key_nip49_with_options", + "pub fn decrypt_secret_key_nip49", + ] { + let authority = if required == "nostr/nip49" { + MANIFEST + } else { + KEY_MODULE + }; + assert!( + authority.contains(required), + "Nostr key authority is missing `{required}`" + ); + } + + for forbidden in ["nostr =", "nip49", "nsec", "ncryptsec", "SecretKey"] { + assert!( + !IDENTITY_MANIFEST.contains(forbidden), + "identity manifest regained Nostr secret ownership `{forbidden}`" + ); + assert!( + !IDENTITY_KEY_MODULE.contains(forbidden), + "identity key module regained Nostr secret ownership `{forbidden}`" + ); + } + + for secret_function in [ + "parse_secret_key", + "secret_key_to_nsec", + "encrypt_secret_key_nip49", + "encrypt_secret_key_nip49_with_options", + "decrypt_secret_key_nip49", + ] { + let signature = format!("pub fn {secret_function}"); + let position = KEY_MODULE + .find(&signature) + .unwrap_or_else(|| panic!("missing secret adapter `{secret_function}`")); + let prefix = &KEY_MODULE[..position]; + let nearby = &prefix[prefix.len().saturating_sub(160)..]; + assert!( + nearby.contains("#[cfg(feature = \"signing\")]"), + "secret adapter `{secret_function}` is not signing-gated" + ); + } +} + fn rust_sources(root: &Path) -> Vec<PathBuf> { let mut pending = vec![root.to_path_buf()]; let mut sources = Vec::new();