lib

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

commit 52b4a3028d5559848de5720988b3f2dd4d0ebbdb
parent 0ac6ce72dd8075441d52fd0ba46490c12c80f748
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 21:00:48 +0000

signer: add client-facing signer capabilities

- add shared signer capability models for local accounts and remote sessions in radroots-nostr-signer
- project selected accounts into signer capabilities and resolve identities through the abstraction in radroots-nostr-accounts
- let radroots-net-core select an explicit signer capability while preserving local key-backed behavior
- validate signer, account, ndb-bridge, and nostr-client paths with targeted tests and nix run .#contract

Diffstat:
Mcrates/net-core/Cargo.toml | 5+++++
Mcrates/net-core/src/net.rs | 47+++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/nostr-accounts/Cargo.toml | 2++
Mcrates/nostr-accounts/src/manager.rs | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mcrates/nostr-accounts/src/ndb_bridge.rs | 28+++++++++++++++++++++++++++-
Acrates/nostr-signer/src/capability.rs | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/nostr-signer/src/lib.rs | 5+++++
7 files changed, 483 insertions(+), 14 deletions(-)

diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml @@ -23,6 +23,7 @@ nostr-client = [ "dep:radroots-events-codec", "radroots-events/serde", "dep:radroots-nostr-accounts", + "dep:radroots-nostr-signer", "dep:secrecy", "dep:hex", "dep:tempfile", @@ -40,6 +41,7 @@ radroots-events = { workspace = true, optional = true, default-features = true, ] } radroots-log = { workspace = true, features = ["std"] } radroots-nostr-accounts = { workspace = true, optional = true, default-features = true } +radroots-nostr-signer = { workspace = true, optional = true } radroots-events-codec = { workspace = true, optional = true, default-features = true, features = [ "std", ] } @@ -58,3 +60,6 @@ thiserror = { workspace = true } tokio = { workspace = true, optional = true, features = ["rt-multi-thread"] } tracing = { workspace = true } futures = { workspace = true } + +[dev-dependencies] +radroots-identity = { workspace = true, default-features = true } diff --git a/crates/net-core/src/net.rs b/crates/net-core/src/net.rs @@ -6,6 +6,8 @@ use crate::error::{NetError, Result}; use crate::nostr_client::{NostrClientManager, NostrConnectionSnapshot}; #[cfg(feature = "nostr-client")] use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; +#[cfg(feature = "nostr-client")] +use radroots_nostr_signer::prelude::RadrootsNostrSignerCapability; #[derive(Debug, Clone, Serialize)] pub struct BuildInfo { @@ -34,6 +36,9 @@ pub struct Net { pub accounts: RadrootsNostrAccountsManager, #[cfg(feature = "nostr-client")] + pub signer: Option<RadrootsNostrSignerCapability>, + + #[cfg(feature = "nostr-client")] pub nostr: Option<NostrClientManager>, #[cfg(feature = "rt")] @@ -57,6 +62,8 @@ impl Net { #[cfg(feature = "nostr-client")] accounts: RadrootsNostrAccountsManager::new_in_memory(), #[cfg(feature = "nostr-client")] + signer: None, + #[cfg(feature = "nostr-client")] nostr: None, #[cfg(feature = "rt")] rt: None, @@ -126,9 +133,22 @@ impl Net { } #[cfg(feature = "nostr-client")] + pub fn set_nostr_signer(&mut self, signer: Option<RadrootsNostrSignerCapability>) { + self.signer = signer; + } + + #[cfg(feature = "nostr-client")] + pub fn selected_nostr_signer(&self) -> Option<RadrootsNostrSignerCapability> { + self.signer + .clone() + .or_else(|| self.accounts.selected_signer_capability().ok().flatten()) + } + + #[cfg(feature = "nostr-client")] pub fn selected_nostr_keys(&self) -> Option<radroots_nostr::prelude::RadrootsNostrKeys> { + let signer = self.selected_nostr_signer()?; self.accounts - .selected_signing_identity() + .resolve_signing_identity_for_signer(&signer) .ok() .flatten() .map(|identity| identity.into_keys()) @@ -151,6 +171,13 @@ impl NetHandle { #[cfg(test)] mod tests { use crate::builder::NetBuilder; + #[cfg(feature = "nostr-client")] + use radroots_identity::RadrootsIdentity; + #[cfg(feature = "nostr-client")] + use radroots_nostr_signer::prelude::{ + RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability, + RadrootsNostrSignerConnectionId, + }; #[test] fn builds_minimal() { @@ -185,12 +212,28 @@ mod tests { #[test] fn selected_nostr_keys_reflects_selected_signing_account() { let cfg = crate::config::NetConfig::default(); - let net = crate::Net::new(cfg); + let mut net = crate::Net::new(cfg); assert!(net.selected_nostr_keys().is_none()); + assert!(net.selected_nostr_signer().is_none()); net.accounts .generate_identity(Some("primary".into()), true) .expect("generate account"); assert!(net.selected_nostr_keys().is_some()); + assert!(net.selected_nostr_signer().is_some()); + + let remote = RadrootsNostrSignerCapability::RemoteSession( + RadrootsNostrRemoteSessionSignerCapability::new( + RadrootsNostrSignerConnectionId::new_v7(), + RadrootsIdentity::generate().to_public(), + RadrootsIdentity::generate().to_public(), + ), + ); + net.set_nostr_signer(Some(remote.clone())); + assert_eq!(net.selected_nostr_signer(), Some(remote)); + assert!(net.selected_nostr_keys().is_none()); + + net.set_nostr_signer(None); + assert!(net.selected_nostr_keys().is_some()); } } diff --git a/crates/nostr-accounts/Cargo.toml b/crates/nostr-accounts/Cargo.toml @@ -19,6 +19,7 @@ std = [ "dep:serde", "dep:serde_json", "dep:radroots-identity", + "dep:radroots-nostr-signer", "dep:radroots-runtime", ] file-store = ["std"] @@ -33,6 +34,7 @@ radroots-identity = { workspace = true, optional = true, default-features = fals "profile", "json-file", ] } +radroots-nostr-signer = { workspace = true, optional = true } radroots-nostr-ndb = { workspace = true, optional = true, default-features = false, features = [ "ndb", "giftwrap", diff --git a/crates/nostr-accounts/src/manager.rs b/crates/nostr-accounts/src/manager.rs @@ -5,6 +5,10 @@ use crate::model::{ use crate::store::{RadrootsNostrAccountStore, RadrootsNostrMemoryAccountStore}; use crate::vault::{RadrootsNostrSecretVault, RadrootsNostrSecretVaultMemory}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityId, RadrootsIdentityPublic}; +use radroots_nostr_signer::prelude::{ + RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, + RadrootsNostrSignerCapability, +}; use std::path::Path; use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -104,17 +108,14 @@ impl RadrootsNostrAccountsManager { return Ok(RadrootsNostrSelectedAccountStatus::NotConfigured); }; - let Some(secret_key_hex) = self.vault.load_secret_hex(&record.account_id)? else { - return Ok(RadrootsNostrSelectedAccountStatus::PublicOnly { account: record }); - }; - - let secret_key_hex = Zeroizing::new(secret_key_hex); - let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())?; - if identity.public_key_hex() != record.public_identity.public_key_hex { - return Err(RadrootsNostrAccountsError::PublicKeyMismatch); - } - - Ok(RadrootsNostrSelectedAccountStatus::Ready { account: record }) + Ok(match self.local_signer_availability(&record)? { + RadrootsNostrLocalSignerAvailability::PublicOnly => { + RadrootsNostrSelectedAccountStatus::PublicOnly { account: record } + } + RadrootsNostrLocalSignerAvailability::SecretBacked => { + RadrootsNostrSelectedAccountStatus::Ready { account: record } + } + }) } pub fn selected_signing_identity( @@ -145,6 +146,46 @@ impl RadrootsNostrAccountsManager { self.resolve_signing_identity(record) } + pub fn selected_signer_capability( + &self, + ) -> Result<Option<RadrootsNostrSignerCapability>, RadrootsNostrAccountsError> { + let Some(record) = self.selected_account()? else { + return Ok(None); + }; + Ok(Some(self.local_signer_capability(record)?)) + } + + pub fn get_signer_capability( + &self, + account_id: &RadrootsIdentityId, + ) -> Result<Option<RadrootsNostrSignerCapability>, RadrootsNostrAccountsError> { + let guard = self.state.read().map_err(|_| { + RadrootsNostrAccountsError::Store("accounts state lock poisoned".into()) + })?; + let Some(record) = guard + .accounts + .iter() + .find(|record| &record.account_id == account_id) + .cloned() + else { + return Ok(None); + }; + drop(guard); + Ok(Some(self.local_signer_capability(record)?)) + } + + pub fn resolve_signing_identity_for_signer( + &self, + signer: &RadrootsNostrSignerCapability, + ) -> Result<Option<RadrootsIdentity>, RadrootsNostrAccountsError> { + match signer { + RadrootsNostrSignerCapability::LocalAccount(capability) => { + self.get_signing_identity(&capability.account_id) + } + RadrootsNostrSignerCapability::RemoteSession(_) => Ok(None), + } + } + pub fn upsert_identity( &self, identity: &RadrootsIdentity, @@ -292,6 +333,36 @@ impl RadrootsNostrAccountsManager { Ok(Some(identity)) } + fn local_signer_capability( + &self, + record: RadrootsNostrAccountRecord, + ) -> Result<RadrootsNostrSignerCapability, RadrootsNostrAccountsError> { + let availability = self.local_signer_availability(&record)?; + Ok(RadrootsNostrSignerCapability::LocalAccount( + RadrootsNostrLocalSignerCapability::new( + record.account_id, + record.public_identity, + availability, + ), + )) + } + + fn local_signer_availability( + &self, + record: &RadrootsNostrAccountRecord, + ) -> Result<RadrootsNostrLocalSignerAvailability, RadrootsNostrAccountsError> { + let Some(secret_key_hex) = self.vault.load_secret_hex(&record.account_id)? else { + return Ok(RadrootsNostrLocalSignerAvailability::PublicOnly); + }; + + let secret_key_hex = Zeroizing::new(secret_key_hex); + let identity = RadrootsIdentity::from_secret_key_str(secret_key_hex.as_str())?; + if identity.public_key_hex() != record.public_identity.public_key_hex { + return Err(RadrootsNostrAccountsError::PublicKeyMismatch); + } + Ok(RadrootsNostrLocalSignerAvailability::SecretBacked) + } + fn update_state( &self, update: impl FnOnce( @@ -589,6 +660,14 @@ mod tests { let account = status_account(&status).expect("account"); assert_eq!(account.account_id, selected_id); assert_eq!(account.label.as_deref(), Some("primary")); + + let signer = manager + .selected_signer_capability() + .expect("selected signer capability") + .expect("signer capability"); + let local = signer.local_account().expect("local signer"); + assert_eq!(local.account_id, selected_id); + assert!(local.is_secret_backed()); } #[test] @@ -687,6 +766,12 @@ mod tests { .expect("selected signing") .is_none() ); + assert!( + manager + .selected_signer_capability() + .expect("selected signer capability") + .is_none() + ); let status = manager .selected_account_status() .expect("selected account status"); @@ -700,6 +785,12 @@ mod tests { .expect("signing") .is_none() ); + assert!( + manager + .get_signer_capability(&missing_id) + .expect("signer capability") + .is_none() + ); } #[test] @@ -973,6 +1064,31 @@ mod tests { .as_deref(), Some("profile-id") ); + + let local_signer = manager + .get_signer_capability(&profile_id) + .expect("local signer capability") + .expect("local signer"); + assert!( + manager + .resolve_signing_identity_for_signer(&local_signer) + .expect("resolve local signer") + .is_some() + ); + + let remote_signer = RadrootsNostrSignerCapability::RemoteSession( + radroots_nostr_signer::prelude::RadrootsNostrRemoteSessionSignerCapability::new( + radroots_nostr_signer::prelude::RadrootsNostrSignerConnectionId::new_v7(), + RadrootsIdentity::generate().to_public(), + RadrootsIdentity::generate().to_public(), + ), + ); + assert!( + manager + .resolve_signing_identity_for_signer(&remote_signer) + .expect("resolve remote signer") + .is_none() + ); } #[test] @@ -1086,12 +1202,20 @@ mod tests { .selected_signing_identity() .expect_err("selected signing poisoned"); assert!(selected_signing_err.to_string().starts_with("store error:")); + let selected_signer_err = manager + .selected_signer_capability() + .expect_err("selected signer poisoned"); + assert!(selected_signer_err.to_string().starts_with("store error:")); let account_id = RadrootsIdentity::generate().id(); let signing_err = manager .get_signing_identity(&account_id) .expect_err("signing poisoned"); assert!(signing_err.to_string().starts_with("store error:")); + let signer_err = manager + .get_signer_capability(&account_id) + .expect_err("signer poisoned"); + assert!(signer_err.to_string().starts_with("store error:")); let select_err = manager .select_account(&account_id) .expect_err("select poisoned"); diff --git a/crates/nostr-accounts/src/ndb_bridge.rs b/crates/nostr-accounts/src/ndb_bridge.rs @@ -6,7 +6,10 @@ pub fn radroots_nostr_accounts_register_selected_secret_with_ndb( manager: &RadrootsNostrAccountsManager, ndb: &RadrootsNostrNdb, ) -> Result<bool, RadrootsNostrAccountsError> { - let Some(identity) = manager.selected_signing_identity()? else { + let Some(signer) = manager.selected_signer_capability()? else { + return Ok(false); + }; + let Some(identity) = manager.resolve_signing_identity_for_signer(&signer)? else { return Ok(false); }; Ok(ndb.add_giftwrap_secret_key(identity.secret_key_bytes())) @@ -38,4 +41,27 @@ mod tests { .expect("register"); assert!(added); } + + #[test] + fn register_selected_secret_returns_false_for_watch_only_account() { + let temp = tempfile::tempdir().expect("tempdir"); + let store = Arc::new(RadrootsNostrFileAccountStore::new( + temp.path().join("accounts.json"), + )); + let vault = Arc::new(RadrootsNostrSecretVaultMemory::new()); + let manager = RadrootsNostrAccountsManager::new(store, vault).expect("manager"); + manager + .upsert_public_identity( + radroots_identity::RadrootsIdentity::generate().to_public(), + Some("watch".into()), + true, + ) + .expect("watch"); + + let ndb = RadrootsNostrNdb::open(RadrootsNostrNdbConfig::new(temp.path().join("ndb"))) + .expect("ndb"); + let added = radroots_nostr_accounts_register_selected_secret_with_ndb(&manager, &ndb) + .expect("register"); + assert!(!added); + } } diff --git a/crates/nostr-signer/src/capability.rs b/crates/nostr-signer/src/capability.rs @@ -0,0 +1,264 @@ +use crate::model::{RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord}; +use nostr::RelayUrl; +use radroots_identity::{RadrootsIdentityId, RadrootsIdentityPublic}; +use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum RadrootsNostrLocalSignerAvailability { + PublicOnly, + SecretBacked, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RadrootsNostrLocalSignerCapability { + pub account_id: RadrootsIdentityId, + pub public_identity: RadrootsIdentityPublic, + pub availability: RadrootsNostrLocalSignerAvailability, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RadrootsNostrRemoteSessionSignerCapability { + pub connection_id: RadrootsNostrSignerConnectionId, + pub signer_identity: RadrootsIdentityPublic, + pub user_identity: RadrootsIdentityPublic, + pub relays: Vec<RelayUrl>, + pub permissions: RadrootsNostrConnectPermissions, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RadrootsNostrSignerCapability { + LocalAccount(RadrootsNostrLocalSignerCapability), + RemoteSession(RadrootsNostrRemoteSessionSignerCapability), +} + +fn public_identity_eq(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) -> bool { + left.id == right.id + && left.public_key_hex == right.public_key_hex + && left.public_key_npub == right.public_key_npub +} + +impl RadrootsNostrLocalSignerCapability { + pub fn new( + account_id: RadrootsIdentityId, + public_identity: RadrootsIdentityPublic, + availability: RadrootsNostrLocalSignerAvailability, + ) -> Self { + Self { + account_id, + public_identity, + availability, + } + } + + pub fn is_secret_backed(&self) -> bool { + self.availability == RadrootsNostrLocalSignerAvailability::SecretBacked + } +} + +impl RadrootsNostrRemoteSessionSignerCapability { + pub fn new( + connection_id: RadrootsNostrSignerConnectionId, + signer_identity: RadrootsIdentityPublic, + user_identity: RadrootsIdentityPublic, + ) -> Self { + Self { + connection_id, + signer_identity, + user_identity, + relays: Vec::new(), + permissions: RadrootsNostrConnectPermissions::default(), + } + } + + pub fn with_relays(mut self, relays: Vec<RelayUrl>) -> Self { + self.relays = relays; + self + } + + pub fn with_permissions(mut self, permissions: RadrootsNostrConnectPermissions) -> Self { + self.permissions = permissions; + self + } +} + +impl RadrootsNostrSignerCapability { + pub fn public_identity(&self) -> &RadrootsIdentityPublic { + match self { + Self::LocalAccount(capability) => &capability.public_identity, + Self::RemoteSession(capability) => &capability.user_identity, + } + } + + pub fn local_account(&self) -> Option<&RadrootsNostrLocalSignerCapability> { + match self { + Self::LocalAccount(capability) => Some(capability), + Self::RemoteSession(_) => None, + } + } + + pub fn remote_session(&self) -> Option<&RadrootsNostrRemoteSessionSignerCapability> { + match self { + Self::RemoteSession(capability) => Some(capability), + Self::LocalAccount(_) => None, + } + } +} + +impl PartialEq for RadrootsNostrLocalSignerCapability { + fn eq(&self, other: &Self) -> bool { + self.account_id == other.account_id + && self.availability == other.availability + && public_identity_eq(&self.public_identity, &other.public_identity) + } +} + +impl Eq for RadrootsNostrLocalSignerCapability {} + +impl PartialEq for RadrootsNostrRemoteSessionSignerCapability { + fn eq(&self, other: &Self) -> bool { + self.connection_id == other.connection_id + && self.relays == other.relays + && self.permissions == other.permissions + && public_identity_eq(&self.signer_identity, &other.signer_identity) + && public_identity_eq(&self.user_identity, &other.user_identity) + } +} + +impl Eq for RadrootsNostrRemoteSessionSignerCapability {} + +impl PartialEq for RadrootsNostrSignerCapability { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::LocalAccount(left), Self::LocalAccount(right)) => left == right, + (Self::RemoteSession(left), Self::RemoteSession(right)) => left == right, + _ => false, + } + } +} + +impl Eq for RadrootsNostrSignerCapability {} + +impl From<&RadrootsNostrSignerConnectionRecord> for RadrootsNostrRemoteSessionSignerCapability { + fn from(value: &RadrootsNostrSignerConnectionRecord) -> Self { + Self { + connection_id: value.connection_id.clone(), + signer_identity: value.signer_identity.clone(), + user_identity: value.user_identity.clone(), + relays: value.relays.clone(), + permissions: value.effective_permissions(), + } + } +} + +impl RadrootsNostrSignerConnectionRecord { + pub fn remote_session_capability(&self) -> RadrootsNostrSignerCapability { + RadrootsNostrSignerCapability::RemoteSession( + RadrootsNostrRemoteSessionSignerCapability::from(self), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{RadrootsNostrSignerConnectionDraft, RadrootsNostrSignerConnectionRecord}; + use nostr::{Keys, PublicKey, RelayUrl, SecretKey}; + use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; + use radroots_nostr_connect::prelude::{ + RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, + }; + + fn public_identity(secret_hex: &str) -> RadrootsIdentityPublic { + RadrootsIdentity::from_secret_key_str(secret_hex) + .expect("identity") + .to_public() + } + + fn public_key(secret_hex: &str) -> PublicKey { + let secret = SecretKey::from_hex(secret_hex).expect("secret"); + Keys::new(secret).public_key() + } + + fn assert_public_identity_matches( + actual: &RadrootsIdentityPublic, + expected: &RadrootsIdentityPublic, + ) { + assert_eq!(actual.id, expected.id); + assert_eq!(actual.public_key_hex, expected.public_key_hex); + assert_eq!(actual.public_key_npub, expected.public_key_npub); + } + + #[test] + fn local_capability_reports_secret_backing_and_public_identity() { + let public_identity = + public_identity("0000000000000000000000000000000000000000000000000000000000000001"); + let capability = + RadrootsNostrSignerCapability::LocalAccount(RadrootsNostrLocalSignerCapability::new( + public_identity.id.clone(), + public_identity.clone(), + RadrootsNostrLocalSignerAvailability::SecretBacked, + )); + + assert_public_identity_matches(capability.public_identity(), &public_identity); + assert!( + capability + .local_account() + .expect("local capability") + .is_secret_backed() + ); + assert!(capability.remote_session().is_none()); + } + + #[test] + fn remote_session_capability_reflects_connection_effective_permissions() { + let signer_identity = + public_identity("0000000000000000000000000000000000000000000000000000000000000002"); + let user_identity = + public_identity("0000000000000000000000000000000000000000000000000000000000000003"); + let record = RadrootsNostrSignerConnectionRecord::new( + RadrootsNostrSignerConnectionId::new_v7(), + signer_identity.clone(), + RadrootsNostrSignerConnectionDraft::new( + public_key("0000000000000000000000000000000000000000000000000000000000000004"), + user_identity.clone(), + ) + .with_requested_permissions( + vec![RadrootsNostrConnectPermission::new( + RadrootsNostrConnectMethod::Ping, + )] + .into(), + ) + .with_relays(vec![RelayUrl::parse("wss://relay.example").expect("relay")]), + 1, + ); + + let capability = record.remote_session_capability(); + assert_public_identity_matches(capability.public_identity(), &user_identity); + let remote = capability.remote_session().expect("remote capability"); + assert_eq!(remote.connection_id, record.connection_id); + assert_public_identity_matches(&remote.signer_identity, &signer_identity); + assert_public_identity_matches(&remote.user_identity, &user_identity); + assert_eq!(remote.permissions, record.effective_permissions()); + assert_eq!(remote.relays, record.relays); + } + + #[test] + fn remote_session_builder_helpers_replace_default_fields() { + let capability = RadrootsNostrRemoteSessionSignerCapability::new( + RadrootsNostrSignerConnectionId::new_v7(), + public_identity("0000000000000000000000000000000000000000000000000000000000000005"), + public_identity("0000000000000000000000000000000000000000000000000000000000000006"), + ) + .with_permissions( + vec![RadrootsNostrConnectPermission::new( + RadrootsNostrConnectMethod::SwitchRelays, + )] + .into(), + ) + .with_relays(vec![RelayUrl::parse("wss://relay.example").expect("relay")]); + + assert_eq!(capability.permissions.as_slice().len(), 1); + assert_eq!(capability.relays.len(), 1); + } +} diff --git a/crates/nostr-signer/src/lib.rs b/crates/nostr-signer/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![forbid(unsafe_code)] +pub mod capability; pub mod error; pub mod evaluation; pub mod manager; @@ -8,6 +9,10 @@ pub mod model; pub mod store; pub mod prelude { + pub use crate::capability::{ + RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, + RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability, + }; pub use crate::error::RadrootsNostrSignerError; pub use crate::evaluation::{ RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal,