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:
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,