input.rs (6028B)
1 use crate::error::RadrootsAppRemoteSignerError; 2 use radroots_identity::RadrootsIdentityPublic; 3 use radroots_nostr_connect::prelude::{ 4 RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions, 5 RadrootsNostrConnectUri, 6 }; 7 use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME; 8 use url::Url; 9 10 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 11 pub enum RadrootsAppRemoteSignerSource { 12 BunkerUri, 13 DiscoveryUrl, 14 } 15 16 #[derive(Debug, Clone)] 17 pub struct RadrootsAppRemoteSignerTarget { 18 pub source: RadrootsAppRemoteSignerSource, 19 pub signer_identity: RadrootsIdentityPublic, 20 pub relays: Vec<String>, 21 pub connect_secret: Option<String>, 22 pub requested_permissions: RadrootsNostrConnectPermissions, 23 } 24 25 impl RadrootsAppRemoteSignerTarget { 26 pub fn source_label(&self) -> &'static str { 27 match self.source { 28 RadrootsAppRemoteSignerSource::BunkerUri => "bunker uri", 29 RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url", 30 } 31 } 32 33 pub fn requested_permission_labels(&self) -> Vec<String> { 34 self.requested_permissions 35 .as_slice() 36 .iter() 37 .map(ToString::to_string) 38 .collect() 39 } 40 } 41 42 pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions { 43 vec![ 44 RadrootsNostrConnectPermission::with_parameter( 45 RadrootsNostrConnectMethod::SignEvent, 46 "kind:1", 47 ), 48 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays), 49 ] 50 .into() 51 } 52 53 pub fn radroots_app_remote_signer_preview( 54 input: &str, 55 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { 56 let trimmed = input.trim(); 57 if trimmed.is_empty() { 58 return Err(RadrootsAppRemoteSignerError::EmptyInput); 59 } 60 61 if trimmed.starts_with(&format!("{RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME}://")) { 62 return parse_bunker_uri(trimmed, RadrootsAppRemoteSignerSource::BunkerUri); 63 } 64 65 if trimmed.starts_with("nostrconnect://") { 66 return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri); 67 } 68 69 parse_discovery_url(trimmed) 70 } 71 72 fn parse_discovery_url( 73 value: &str, 74 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { 75 let url = Url::parse(value) 76 .map_err(|error| RadrootsAppRemoteSignerError::InvalidDiscoveryUrl(error.to_string()))?; 77 let Some((_, bunker_uri)) = url.query_pairs().find(|(key, _)| key == "uri") else { 78 return Err(RadrootsAppRemoteSignerError::MissingDiscoveryUri); 79 }; 80 parse_bunker_uri( 81 bunker_uri.as_ref(), 82 RadrootsAppRemoteSignerSource::DiscoveryUrl, 83 ) 84 } 85 86 fn parse_bunker_uri( 87 value: &str, 88 source: RadrootsAppRemoteSignerSource, 89 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> { 90 let uri = RadrootsNostrConnectUri::parse(value)?; 91 let RadrootsNostrConnectUri::Bunker(bunker_uri) = uri else { 92 return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri); 93 }; 94 Ok(RadrootsAppRemoteSignerTarget { 95 source, 96 signer_identity: RadrootsIdentityPublic::new(bunker_uri.remote_signer_public_key), 97 relays: bunker_uri 98 .relays 99 .into_iter() 100 .map(|relay| relay.to_string()) 101 .collect(), 102 connect_secret: bunker_uri.secret, 103 requested_permissions: radroots_app_remote_signer_requested_permissions(), 104 }) 105 } 106 107 #[cfg(test)] 108 mod tests { 109 use super::*; 110 use radroots_identity::RadrootsIdentity; 111 112 const RELAY_PRIMARY_WSS: &str = "wss://relay.example.com"; 113 const SIGNER_SECRET_KEY_HEX: &str = 114 "1111111111111111111111111111111111111111111111111111111111111111"; 115 116 fn signer_identity() -> RadrootsIdentity { 117 RadrootsIdentity::from_secret_key_str(SIGNER_SECRET_KEY_HEX).expect("identity") 118 } 119 120 fn bunker_uri() -> String { 121 format!( 122 "bunker://{}?relay={}", 123 signer_identity().public_key_hex(), 124 urlencoding(RELAY_PRIMARY_WSS) 125 ) 126 } 127 128 fn discovery_url() -> String { 129 format!( 130 "http://localhost/connect?uri={}", 131 urlencoding(bunker_uri().as_str()) 132 ) 133 } 134 135 fn urlencoding(value: &str) -> String { 136 url::form_urlencoded::byte_serialize(value.as_bytes()).collect() 137 } 138 139 #[test] 140 fn parses_direct_bunker_uri() { 141 let preview = radroots_app_remote_signer_preview(bunker_uri().as_str()).expect("preview"); 142 143 assert_eq!(preview.source, RadrootsAppRemoteSignerSource::BunkerUri); 144 assert_eq!( 145 preview.signer_identity.public_key_hex, 146 signer_identity().public_key_hex() 147 ); 148 assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); 149 assert_eq!(preview.connect_secret, None); 150 assert_eq!( 151 preview.requested_permission_labels(), 152 vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()] 153 ); 154 } 155 156 #[test] 157 fn parses_discovery_url_with_bunker_uri() { 158 let preview = 159 radroots_app_remote_signer_preview(discovery_url().as_str()).expect("preview"); 160 161 assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl); 162 assert_eq!( 163 preview.signer_identity.public_key_hex, 164 signer_identity().public_key_hex() 165 ); 166 assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]); 167 assert_eq!( 168 preview.requested_permission_labels(), 169 vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()] 170 ); 171 } 172 173 #[test] 174 fn rejects_client_side_nostrconnect_uri_input() { 175 let err = radroots_app_remote_signer_preview( 176 "nostrconnect://npub1test?relay=wss%3A%2F%2Frelay.example.com&secret=test", 177 ) 178 .expect_err("client uri rejected"); 179 180 assert_eq!(err, RadrootsAppRemoteSignerError::UnsupportedClientUri); 181 } 182 }