app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 053a2aeffc2546aaf8ab504ea2c3f2214f48fbae
parent 10577cbff24e0d97965611a99c0b1b49e206f25d
Author: triesap <tyson@radroots.org>
Date:   Thu,  2 Apr 2026 00:49:37 +0000

remote-signer: request explicit review permissions

- add a shared requested permission contract for bunker and discovery remote signer targets
- send explicit sign_event:kind:1 permissions in the nip46 connect request instead of an empty default set
- surface the same requested permissions in desktop ios and android preview review flows
- cover the shared request and preview mapping with remote signer core and desktop validation

Diffstat:
Mcrates/android/src/remote_signer.rs | 3++-
Mcrates/core/src/lib.rs | 2+-
Mcrates/desktop/src/remote_signer.rs | 7++++++-
Mcrates/ios/src/remote_signer.rs | 3++-
Mcrates/remote-signer/src/input.rs | 31++++++++++++++++++++++++++++++-
Mcrates/remote-signer/src/lib.rs | 2+-
Mcrates/remote-signer/src/protocol.rs | 51+++++++++++++++++++++++++++++++++++++++++++--------
7 files changed, 85 insertions(+), 14 deletions(-)

diff --git a/crates/android/src/remote_signer.rs b/crates/android/src/remote_signer.rs @@ -147,11 +147,12 @@ impl AndroidRemoteSigner { pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; + let requested_permissions = preview.requested_permission_labels(); Ok(RadrootsRemoteSignerPreview { source_label: preview.source_label().to_owned(), signer_npub: preview.signer_identity.public_key_npub, relays: preview.relays, - requested_permissions: Vec::new(), + requested_permissions, }) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -1743,7 +1743,7 @@ mod tests { source_label: "discovery url".into(), signer_npub: FIXTURE_BOB.npub.into(), relays: vec!["ws://localhost:8080".into()], - requested_permissions: Vec::new(), + requested_permissions: vec!["sign_event:kind:1".into()], } } diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs @@ -146,11 +146,12 @@ impl DesktopRemoteSigner { pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; + let requested_permissions = preview.requested_permission_labels(); Ok(RadrootsRemoteSignerPreview { source_label: preview.source_label().to_owned(), signer_npub: preview.signer_identity.public_key_npub, relays: preview.relays, - requested_permissions: Vec::new(), + requested_permissions, }) } @@ -358,5 +359,9 @@ mod tests { assert_eq!(preview.source_label, "discovery url"); assert_eq!(preview.signer_npub, FIXTURE_BOB.npub); assert_eq!(preview.relays, vec!["ws://localhost:8080".to_owned()]); + assert_eq!( + preview.requested_permissions, + vec!["sign_event:kind:1".to_owned()] + ); } } diff --git a/crates/ios/src/remote_signer.rs b/crates/ios/src/remote_signer.rs @@ -147,11 +147,12 @@ impl IosRemoteSigner { pub(crate) fn preview_connection(input: &str) -> Result<RadrootsRemoteSignerPreview, String> { let preview = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; + let requested_permissions = preview.requested_permission_labels(); Ok(RadrootsRemoteSignerPreview { source_label: preview.source_label().to_owned(), signer_npub: preview.signer_identity.public_key_npub, relays: preview.relays, - requested_permissions: Vec::new(), + requested_permissions, }) } diff --git a/crates/remote-signer/src/input.rs b/crates/remote-signer/src/input.rs @@ -1,6 +1,9 @@ use crate::error::RadrootsAppRemoteSignerError; use radroots_identity::RadrootsIdentityPublic; -use radroots_nostr_connect::prelude::RadrootsNostrConnectUri; +use radroots_nostr_connect::prelude::{ + RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, + RadrootsNostrConnectUri, +}; use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME; use url::Url; @@ -16,6 +19,7 @@ pub struct RadrootsAppRemoteSignerTarget { pub signer_identity: RadrootsIdentityPublic, pub relays: Vec<String>, pub connect_secret: Option<String>, + pub requested_permissions: RadrootsNostrConnectPermissions, } impl RadrootsAppRemoteSignerTarget { @@ -25,6 +29,22 @@ impl RadrootsAppRemoteSignerTarget { RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url", } } + + pub fn requested_permission_labels(&self) -> Vec<String> { + self.requested_permissions + .as_slice() + .iter() + .map(ToString::to_string) + .collect() + } +} + +pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions { + vec![RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + "kind:1", + )] + .into() } pub fn radroots_app_remote_signer_preview( @@ -77,6 +97,7 @@ fn parse_bunker_uri( .map(|relay| relay.to_string()) .collect(), connect_secret: bunker_uri.secret, + requested_permissions: radroots_app_remote_signer_requested_permissions(), }) } @@ -112,6 +133,10 @@ mod tests { assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub); assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); assert_eq!(preview.connect_secret, None); + assert_eq!( + preview.requested_permission_labels(), + vec!["sign_event:kind:1".to_owned()] + ); } #[test] @@ -122,6 +147,10 @@ mod tests { assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl); assert_eq!(preview.signer_identity.public_key_npub, FIXTURE_ALICE.npub); assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); + assert_eq!( + preview.requested_permission_labels(), + vec!["sign_event:kind:1".to_owned()] + ); } #[test] diff --git a/crates/remote-signer/src/lib.rs b/crates/remote-signer/src/lib.rs @@ -22,7 +22,7 @@ pub use custody::{ pub use error::RadrootsAppRemoteSignerError; pub use input::{ RadrootsAppRemoteSignerSource, RadrootsAppRemoteSignerTarget, - radroots_app_remote_signer_preview, + radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions, }; pub use protocol::{ RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerPendingSession, diff --git a/crates/remote-signer/src/protocol.rs b/crates/remote-signer/src/protocol.rs @@ -66,17 +66,12 @@ async fn connect_pending_session( target: RadrootsAppRemoteSignerTarget, ) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { let client_identity = RadrootsIdentity::generate(); + let connect_request = connect_request_for_target(&target)?; let response = execute_request( &client_identity, &target, RadrootsNostrConnectMethod::Connect, - RadrootsNostrConnectRequest::Connect { - remote_signer_public_key: parse_public_key_hex( - target.signer_identity.public_key_hex.as_str(), - )?, - secret: target.connect_secret.clone(), - requested_permissions: Default::default(), - }, + connect_request, CONNECT_TIMEOUT, ) .await?; @@ -100,6 +95,18 @@ async fn connect_pending_session( } } +fn connect_request_for_target( + target: &RadrootsAppRemoteSignerTarget, +) -> Result<RadrootsNostrConnectRequest, RadrootsAppRemoteSignerError> { + Ok(RadrootsNostrConnectRequest::Connect { + remote_signer_public_key: parse_public_key_hex( + target.signer_identity.public_key_hex.as_str(), + )?, + secret: target.connect_secret.clone(), + requested_permissions: target.requested_permissions.clone(), + }) +} + async fn poll_pending_session( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, @@ -111,6 +118,7 @@ async fn poll_pending_session( signer_identity: record.signer_identity.clone(), relays: record.relays.clone(), connect_secret: None, + requested_permissions: crate::radroots_app_remote_signer_requested_permissions(), }; match execute_request( @@ -338,8 +346,9 @@ fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsAppRemo #[cfg(test)] mod tests { use super::*; + use crate::radroots_app_remote_signer_preview; use nostr::PublicKey; - use radroots_app_test_support::{FIXTURE_ALICE, fixture_identity}; + use radroots_app_test_support::{FIXTURE_ALICE, RELAY_PRIMARY_WSS, fixture_identity}; fn fixture_public_key() -> PublicKey { fixture_identity(&FIXTURE_ALICE) @@ -347,6 +356,16 @@ mod tests { .public_key() } + fn fixture_discovery_url() -> String { + format!( + "http://localhost/connect?uri={}", + url::form_urlencoded::byte_serialize( + format!("bunker://{}?relay={RELAY_PRIMARY_WSS}", FIXTURE_ALICE.npub).as_bytes() + ) + .collect::<String>() + ) + } + #[test] fn pending_connection_response_is_classified_as_pending_approval() { let outcome = @@ -412,4 +431,20 @@ mod tests { if message.contains("unexpected `get_public_key` response") )); } + + #[test] + fn connect_request_uses_explicit_requested_permissions() { + let target = + radroots_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview"); + + let request = connect_request_for_target(&target).expect("request"); + + match request { + RadrootsNostrConnectRequest::Connect { + requested_permissions, + .. + } => assert_eq!(requested_permissions.to_string(), "sign_event:kind:1"), + other => panic!("unexpected request: {other:?}"), + } + } }