capability.rs (13920B)
1 use crate::model::{RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord}; 2 use nostr::RelayUrl; 3 use radroots_identity::{RadrootsIdentityId, RadrootsIdentityPublic}; 4 use radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions; 5 use serde::{Deserialize, Serialize}; 6 7 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 8 pub enum RadrootsNostrLocalSignerAvailability { 9 PublicOnly, 10 SecretBacked, 11 } 12 13 #[derive(Debug, Clone, Serialize, Deserialize)] 14 pub struct RadrootsNostrLocalSignerCapability { 15 pub account_id: RadrootsIdentityId, 16 pub public_identity: RadrootsIdentityPublic, 17 pub availability: RadrootsNostrLocalSignerAvailability, 18 } 19 20 #[derive(Debug, Clone, Serialize, Deserialize)] 21 pub struct RadrootsNostrRemoteSessionSignerCapability { 22 pub connection_id: RadrootsNostrSignerConnectionId, 23 pub signer_identity: RadrootsIdentityPublic, 24 pub user_identity: RadrootsIdentityPublic, 25 pub relays: Vec<RelayUrl>, 26 pub permissions: RadrootsNostrConnectPermissions, 27 } 28 29 #[derive(Debug, Clone, Serialize, Deserialize)] 30 pub enum RadrootsNostrSignerCapability { 31 LocalAccount(Box<RadrootsNostrLocalSignerCapability>), 32 RemoteSession(Box<RadrootsNostrRemoteSessionSignerCapability>), 33 } 34 35 fn public_identity_eq(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) -> bool { 36 left.id == right.id 37 && left.public_key_hex == right.public_key_hex 38 && left.public_key_npub == right.public_key_npub 39 } 40 41 impl RadrootsNostrLocalSignerCapability { 42 pub fn new( 43 account_id: RadrootsIdentityId, 44 public_identity: RadrootsIdentityPublic, 45 availability: RadrootsNostrLocalSignerAvailability, 46 ) -> Self { 47 Self { 48 account_id, 49 public_identity, 50 availability, 51 } 52 } 53 54 pub fn is_secret_backed(&self) -> bool { 55 self.availability == RadrootsNostrLocalSignerAvailability::SecretBacked 56 } 57 } 58 59 impl RadrootsNostrRemoteSessionSignerCapability { 60 pub fn new( 61 connection_id: RadrootsNostrSignerConnectionId, 62 signer_identity: RadrootsIdentityPublic, 63 user_identity: RadrootsIdentityPublic, 64 ) -> Self { 65 Self { 66 connection_id, 67 signer_identity, 68 user_identity, 69 relays: Vec::new(), 70 permissions: RadrootsNostrConnectPermissions::default(), 71 } 72 } 73 74 pub fn with_relays(mut self, relays: Vec<RelayUrl>) -> Self { 75 self.relays = relays; 76 self 77 } 78 79 pub fn with_permissions(mut self, permissions: RadrootsNostrConnectPermissions) -> Self { 80 self.permissions = permissions; 81 self 82 } 83 } 84 85 impl RadrootsNostrSignerCapability { 86 pub fn public_identity(&self) -> &RadrootsIdentityPublic { 87 match self { 88 Self::LocalAccount(capability) => &capability.public_identity, 89 Self::RemoteSession(capability) => &capability.user_identity, 90 } 91 } 92 93 pub fn local_account(&self) -> Option<&RadrootsNostrLocalSignerCapability> { 94 match self { 95 Self::LocalAccount(capability) => Some(capability.as_ref()), 96 Self::RemoteSession(_) => None, 97 } 98 } 99 100 pub fn remote_session(&self) -> Option<&RadrootsNostrRemoteSessionSignerCapability> { 101 match self { 102 Self::RemoteSession(capability) => Some(capability.as_ref()), 103 Self::LocalAccount(_) => None, 104 } 105 } 106 } 107 108 impl PartialEq for RadrootsNostrLocalSignerCapability { 109 fn eq(&self, other: &Self) -> bool { 110 self.account_id == other.account_id 111 && self.availability == other.availability 112 && public_identity_eq(&self.public_identity, &other.public_identity) 113 } 114 } 115 116 impl Eq for RadrootsNostrLocalSignerCapability {} 117 118 impl PartialEq for RadrootsNostrRemoteSessionSignerCapability { 119 fn eq(&self, other: &Self) -> bool { 120 self.connection_id == other.connection_id 121 && self.relays == other.relays 122 && self.permissions == other.permissions 123 && public_identity_eq(&self.signer_identity, &other.signer_identity) 124 && public_identity_eq(&self.user_identity, &other.user_identity) 125 } 126 } 127 128 impl Eq for RadrootsNostrRemoteSessionSignerCapability {} 129 130 impl PartialEq for RadrootsNostrSignerCapability { 131 fn eq(&self, other: &Self) -> bool { 132 match (self, other) { 133 (Self::LocalAccount(left), Self::LocalAccount(right)) => { 134 left.as_ref() == right.as_ref() 135 } 136 (Self::RemoteSession(left), Self::RemoteSession(right)) => { 137 left.as_ref() == right.as_ref() 138 } 139 _ => false, 140 } 141 } 142 } 143 144 impl Eq for RadrootsNostrSignerCapability {} 145 146 impl From<&RadrootsNostrSignerConnectionRecord> for RadrootsNostrRemoteSessionSignerCapability { 147 fn from(value: &RadrootsNostrSignerConnectionRecord) -> Self { 148 Self { 149 connection_id: value.connection_id.clone(), 150 signer_identity: value.signer_identity.clone(), 151 user_identity: value.user_identity.clone(), 152 relays: value.relays.clone(), 153 permissions: value.effective_permissions(), 154 } 155 } 156 } 157 158 impl RadrootsNostrSignerConnectionRecord { 159 pub fn remote_session_capability(&self) -> RadrootsNostrSignerCapability { 160 RadrootsNostrSignerCapability::RemoteSession(Box::new( 161 RadrootsNostrRemoteSessionSignerCapability::from(self), 162 )) 163 } 164 } 165 166 #[cfg(test)] 167 mod tests { 168 use super::*; 169 use crate::model::{RadrootsNostrSignerConnectionDraft, RadrootsNostrSignerConnectionRecord}; 170 use crate::test_support::{ 171 fixture_alice_identity, fixture_bob_identity, fixture_carol_identity, 172 fixture_diego_public_key, primary_relay, secondary_relay, 173 }; 174 use radroots_identity::RadrootsIdentityPublic; 175 use radroots_nostr_connect::prelude::{ 176 RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, 177 }; 178 179 fn assert_public_identity_matches( 180 actual: &RadrootsIdentityPublic, 181 expected: &RadrootsIdentityPublic, 182 ) { 183 assert_eq!(actual.id, expected.id); 184 assert_eq!(actual.public_key_hex, expected.public_key_hex); 185 assert_eq!(actual.public_key_npub, expected.public_key_npub); 186 } 187 188 #[test] 189 fn local_capability_reports_secret_backing_and_public_identity() { 190 let public_identity = fixture_alice_identity(); 191 let capability = RadrootsNostrSignerCapability::LocalAccount(Box::new( 192 RadrootsNostrLocalSignerCapability::new( 193 public_identity.id.clone(), 194 public_identity.clone(), 195 RadrootsNostrLocalSignerAvailability::SecretBacked, 196 ), 197 )); 198 199 assert_public_identity_matches(capability.public_identity(), &public_identity); 200 assert!( 201 capability 202 .local_account() 203 .expect("local capability") 204 .is_secret_backed() 205 ); 206 assert!(capability.remote_session().is_none()); 207 } 208 209 #[test] 210 fn remote_session_capability_reflects_connection_effective_permissions() { 211 let signer_identity = fixture_bob_identity(); 212 let user_identity = fixture_carol_identity(); 213 let record = RadrootsNostrSignerConnectionRecord::new( 214 RadrootsNostrSignerConnectionId::new_v7(), 215 signer_identity.clone(), 216 RadrootsNostrSignerConnectionDraft::new( 217 fixture_diego_public_key(), 218 user_identity.clone(), 219 ) 220 .with_requested_permissions( 221 vec![RadrootsNostrConnectPermission::new( 222 RadrootsNostrConnectMethod::Ping, 223 )] 224 .into(), 225 ) 226 .with_relays(vec![primary_relay()]), 227 1, 228 ); 229 230 let capability = record.remote_session_capability(); 231 assert_public_identity_matches(capability.public_identity(), &user_identity); 232 assert!(capability.local_account().is_none()); 233 let remote = capability.remote_session().expect("remote capability"); 234 assert_eq!(remote.connection_id, record.connection_id); 235 assert_public_identity_matches(&remote.signer_identity, &signer_identity); 236 assert_public_identity_matches(&remote.user_identity, &user_identity); 237 assert_eq!(remote.permissions, record.effective_permissions()); 238 assert_eq!(remote.relays, record.relays); 239 } 240 241 #[test] 242 fn remote_session_builder_helpers_replace_default_fields() { 243 let capability = RadrootsNostrRemoteSessionSignerCapability::new( 244 RadrootsNostrSignerConnectionId::new_v7(), 245 fixture_alice_identity(), 246 fixture_bob_identity(), 247 ) 248 .with_permissions( 249 vec![RadrootsNostrConnectPermission::new( 250 RadrootsNostrConnectMethod::SwitchRelays, 251 )] 252 .into(), 253 ) 254 .with_relays(vec![primary_relay()]); 255 256 assert_eq!(capability.permissions.as_slice().len(), 1); 257 assert_eq!(capability.relays.len(), 1); 258 } 259 260 #[test] 261 fn capability_equality_accounts_for_identity_fields_and_variant_kind() { 262 let alice = fixture_alice_identity(); 263 let mut alice_with_different_hex = alice.clone(); 264 alice_with_different_hex.public_key_hex = fixture_bob_identity().public_key_hex; 265 let mut alice_with_different_npub = alice.clone(); 266 alice_with_different_npub.public_key_npub = fixture_bob_identity().public_key_npub; 267 268 let local = RadrootsNostrLocalSignerCapability::new( 269 alice.id.clone(), 270 alice.clone(), 271 RadrootsNostrLocalSignerAvailability::SecretBacked, 272 ); 273 let local_same = RadrootsNostrLocalSignerCapability::new( 274 alice.id.clone(), 275 alice.clone(), 276 RadrootsNostrLocalSignerAvailability::SecretBacked, 277 ); 278 let local_changed_account = RadrootsNostrLocalSignerCapability::new( 279 fixture_bob_identity().id, 280 alice.clone(), 281 RadrootsNostrLocalSignerAvailability::SecretBacked, 282 ); 283 let local_changed_availability = RadrootsNostrLocalSignerCapability::new( 284 alice.id.clone(), 285 alice.clone(), 286 RadrootsNostrLocalSignerAvailability::PublicOnly, 287 ); 288 let local_changed_hex = RadrootsNostrLocalSignerCapability::new( 289 alice.id.clone(), 290 alice_with_different_hex, 291 RadrootsNostrLocalSignerAvailability::SecretBacked, 292 ); 293 let local_changed = RadrootsNostrLocalSignerCapability::new( 294 alice.id.clone(), 295 alice_with_different_npub, 296 RadrootsNostrLocalSignerAvailability::SecretBacked, 297 ); 298 assert_eq!(local, local_same); 299 assert_ne!(local, local_changed_account); 300 assert_ne!(local, local_changed_availability); 301 assert_ne!(local, local_changed_hex); 302 assert_ne!(local, local_changed); 303 304 let remote = RadrootsNostrRemoteSessionSignerCapability::new( 305 RadrootsNostrSignerConnectionId::new_v7(), 306 fixture_bob_identity(), 307 fixture_carol_identity(), 308 ) 309 .with_relays(vec![primary_relay()]); 310 let remote_same = remote.clone(); 311 let remote_changed_connection = RadrootsNostrRemoteSessionSignerCapability::new( 312 RadrootsNostrSignerConnectionId::new_v7(), 313 remote.signer_identity.clone(), 314 remote.user_identity.clone(), 315 ) 316 .with_relays(remote.relays.clone()) 317 .with_permissions(remote.permissions.clone()); 318 let remote_changed_relays = remote.clone().with_relays(vec![secondary_relay()]); 319 let remote_changed_permissions = remote.clone().with_permissions( 320 vec![RadrootsNostrConnectPermission::new( 321 RadrootsNostrConnectMethod::Ping, 322 )] 323 .into(), 324 ); 325 let mut remote_changed_signer = remote.clone(); 326 remote_changed_signer.signer_identity.public_key_hex = 327 fixture_alice_identity().public_key_hex; 328 let mut remote_changed = remote.clone(); 329 remote_changed.user_identity.public_key_npub = fixture_alice_identity().public_key_npub; 330 assert_eq!(remote, remote_same); 331 assert_ne!(remote, remote_changed_connection); 332 assert_ne!(remote, remote_changed_relays); 333 assert_ne!(remote, remote_changed_permissions); 334 assert_ne!(remote, remote_changed_signer); 335 assert_ne!(remote, remote_changed); 336 337 assert_eq!( 338 RadrootsNostrSignerCapability::LocalAccount(Box::new(local.clone())), 339 RadrootsNostrSignerCapability::LocalAccount(Box::new(local_same)) 340 ); 341 assert_eq!( 342 RadrootsNostrSignerCapability::RemoteSession(Box::new(remote.clone())), 343 RadrootsNostrSignerCapability::RemoteSession(Box::new(remote)) 344 ); 345 assert_ne!( 346 RadrootsNostrSignerCapability::LocalAccount(Box::new(local)), 347 RadrootsNostrSignerCapability::RemoteSession(Box::new(remote_changed)) 348 ); 349 } 350 351 #[test] 352 fn public_identity_eq_covers_field_level_short_circuits() { 353 let alice = fixture_alice_identity(); 354 let bob = fixture_bob_identity(); 355 356 let mut different_id = alice.clone(); 357 different_id.id = bob.id.clone(); 358 assert!(!public_identity_eq(&alice, &different_id)); 359 360 let mut different_hex = alice.clone(); 361 different_hex.public_key_hex = bob.public_key_hex.clone(); 362 assert!(!public_identity_eq(&alice, &different_hex)); 363 364 let mut different_npub = alice.clone(); 365 different_npub.public_key_npub = bob.public_key_npub.clone(); 366 assert!(!public_identity_eq(&alice, &different_npub)); 367 368 assert!(public_identity_eq(&alice, &alice)); 369 } 370 }