lib

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

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 }