backend.rs (69684B)
1 use crate::capability::{ 2 RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, 3 RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability, 4 }; 5 use crate::error::RadrootsNostrSignerError; 6 use crate::evaluation::{ 7 RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerRequestEvaluation, 8 RadrootsNostrSignerSessionLookup, 9 }; 10 use crate::manager::RadrootsNostrSignerManager; 11 use crate::model::{ 12 RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerConnectionDraft, 13 RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord, 14 RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerPendingRequest, 15 RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerRequestAuditRecord, 16 RadrootsNostrSignerRequestDecision, RadrootsNostrSignerWorkflowId, 17 }; 18 use nostr::{Event, EventBuilder, PublicKey, RelayUrl, UnsignedEvent}; 19 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; 20 use radroots_nostr_connect::prelude::{ 21 RadrootsNostrConnectMethod, RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, 22 RadrootsNostrConnectRequestMessage, 23 }; 24 use serde::{Deserialize, Serialize}; 25 26 #[derive(Debug, Clone, Serialize, Deserialize)] 27 pub struct RadrootsNostrSignerBackendCapabilities { 28 #[serde(default, skip_serializing_if = "Option::is_none")] 29 pub local_signer: Option<RadrootsNostrLocalSignerCapability>, 30 #[serde(default)] 31 pub remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>, 32 } 33 34 #[derive(Debug, Clone, Serialize, Deserialize)] 35 pub struct RadrootsNostrSignerSignOutput { 36 pub signer: RadrootsNostrSignerCapability, 37 pub event: Event, 38 } 39 40 #[derive(Debug, Clone, Serialize, Deserialize)] 41 #[serde(rename_all = "snake_case", tag = "state", content = "value")] 42 pub enum RadrootsNostrSignerPublishTransition { 43 Begun(RadrootsNostrSignerPublishWorkflowRecord), 44 MarkedPublished(RadrootsNostrSignerPublishWorkflowRecord), 45 Finalized { 46 workflow_id: RadrootsNostrSignerWorkflowId, 47 connection: Box<RadrootsNostrSignerConnectionRecord>, 48 }, 49 Cancelled(RadrootsNostrSignerPublishWorkflowRecord), 50 } 51 52 pub trait RadrootsNostrSignerBackend: Send + Sync { 53 fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError>; 54 55 fn set_signer_identity( 56 &self, 57 signer_identity: RadrootsIdentityPublic, 58 ) -> Result<(), RadrootsNostrSignerError>; 59 60 fn capabilities( 61 &self, 62 ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError>; 63 64 fn list_connections( 65 &self, 66 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>; 67 68 fn get_connection( 69 &self, 70 connection_id: &RadrootsNostrSignerConnectionId, 71 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>; 72 73 fn list_publish_workflows( 74 &self, 75 ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>; 76 77 fn get_publish_workflow( 78 &self, 79 workflow_id: &RadrootsNostrSignerWorkflowId, 80 ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError>; 81 82 fn find_connections_by_client_public_key( 83 &self, 84 client_public_key: &PublicKey, 85 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>; 86 87 fn find_connection_by_connect_secret( 88 &self, 89 connect_secret: &str, 90 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError>; 91 92 fn lookup_session( 93 &self, 94 client_public_key: &PublicKey, 95 connect_secret: Option<&str>, 96 ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError>; 97 98 fn evaluate_connect_request( 99 &self, 100 client_public_key: PublicKey, 101 request: RadrootsNostrConnectRequest, 102 ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError>; 103 104 fn register_connection( 105 &self, 106 draft: RadrootsNostrSignerConnectionDraft, 107 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 108 109 fn set_granted_permissions( 110 &self, 111 connection_id: &RadrootsNostrSignerConnectionId, 112 granted_permissions: RadrootsNostrConnectPermissions, 113 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 114 115 fn approve_connection( 116 &self, 117 connection_id: &RadrootsNostrSignerConnectionId, 118 granted_permissions: RadrootsNostrConnectPermissions, 119 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 120 121 fn reject_connection( 122 &self, 123 connection_id: &RadrootsNostrSignerConnectionId, 124 reason: Option<String>, 125 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 126 127 fn revoke_connection( 128 &self, 129 connection_id: &RadrootsNostrSignerConnectionId, 130 reason: Option<String>, 131 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 132 133 fn update_relays( 134 &self, 135 connection_id: &RadrootsNostrSignerConnectionId, 136 relays: Vec<RelayUrl>, 137 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 138 139 fn require_auth_challenge( 140 &self, 141 connection_id: &RadrootsNostrSignerConnectionId, 142 auth_url: &str, 143 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 144 145 fn set_pending_request( 146 &self, 147 connection_id: &RadrootsNostrSignerConnectionId, 148 request_message: RadrootsNostrConnectRequestMessage, 149 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 150 151 fn authorize_auth_challenge( 152 &self, 153 connection_id: &RadrootsNostrSignerConnectionId, 154 ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError>; 155 156 fn restore_pending_auth_challenge( 157 &self, 158 connection_id: &RadrootsNostrSignerConnectionId, 159 pending_request: RadrootsNostrSignerPendingRequest, 160 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 161 162 fn begin_connect_secret_publish_finalization( 163 &self, 164 connection_id: &RadrootsNostrSignerConnectionId, 165 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>; 166 167 fn begin_auth_replay_publish_finalization( 168 &self, 169 connection_id: &RadrootsNostrSignerConnectionId, 170 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>; 171 172 fn mark_publish_workflow_published( 173 &self, 174 workflow_id: &RadrootsNostrSignerWorkflowId, 175 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>; 176 177 fn finalize_publish_workflow( 178 &self, 179 workflow_id: &RadrootsNostrSignerWorkflowId, 180 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>; 181 182 fn cancel_publish_workflow( 183 &self, 184 workflow_id: &RadrootsNostrSignerWorkflowId, 185 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError>; 186 187 fn mark_authenticated( 188 &self, 189 connection_id: &RadrootsNostrSignerConnectionId, 190 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 191 192 fn mark_connect_secret_consumed( 193 &self, 194 connection_id: &RadrootsNostrSignerConnectionId, 195 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError>; 196 197 fn evaluate_request( 198 &self, 199 connection_id: &RadrootsNostrSignerConnectionId, 200 request_message: RadrootsNostrConnectRequestMessage, 201 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>; 202 203 fn evaluate_auth_replay_publish_workflow( 204 &self, 205 workflow_id: &RadrootsNostrSignerWorkflowId, 206 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError>; 207 208 fn record_request( 209 &self, 210 connection_id: &RadrootsNostrSignerConnectionId, 211 request_id: &str, 212 method: RadrootsNostrConnectMethod, 213 decision: RadrootsNostrSignerRequestDecision, 214 message: Option<String>, 215 ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError>; 216 217 fn sign_unsigned_event( 218 &self, 219 unsigned_event: UnsignedEvent, 220 ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError>; 221 222 fn sign_event_builder( 223 &self, 224 builder: EventBuilder, 225 ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> { 226 let signer_identity = self 227 .signer_identity()? 228 .ok_or(RadrootsNostrSignerError::MissingSignerIdentity)?; 229 let public_key = parse_identity_public_key(&signer_identity)?; 230 self.sign_unsigned_event(builder.build(public_key)) 231 } 232 } 233 234 #[derive(Clone)] 235 pub struct RadrootsNostrEmbeddedSignerBackend { 236 manager: RadrootsNostrSignerManager, 237 signer_identity: RadrootsIdentity, 238 } 239 240 impl RadrootsNostrSignerBackendCapabilities { 241 pub fn new( 242 local_signer: Option<RadrootsNostrLocalSignerCapability>, 243 remote_sessions: Vec<RadrootsNostrRemoteSessionSignerCapability>, 244 ) -> Self { 245 Self { 246 local_signer, 247 remote_sessions, 248 } 249 } 250 251 pub fn all_signers(&self) -> Vec<RadrootsNostrSignerCapability> { 252 let mut signers = Vec::new(); 253 if let Some(local_signer) = self.local_signer.clone() { 254 signers.push(RadrootsNostrSignerCapability::LocalAccount(Box::new( 255 local_signer, 256 ))); 257 } 258 signers.extend( 259 self.remote_sessions 260 .iter() 261 .cloned() 262 .map(Box::new) 263 .map(RadrootsNostrSignerCapability::RemoteSession), 264 ); 265 signers 266 } 267 } 268 269 impl RadrootsNostrSignerSignOutput { 270 pub fn new(signer: RadrootsNostrSignerCapability, event: Event) -> Self { 271 Self { signer, event } 272 } 273 } 274 275 impl RadrootsNostrSignerPublishTransition { 276 pub fn begun(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self { 277 Self::Begun(workflow) 278 } 279 280 pub fn marked_published(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self { 281 Self::MarkedPublished(workflow) 282 } 283 284 pub fn finalized( 285 workflow_id: RadrootsNostrSignerWorkflowId, 286 connection: RadrootsNostrSignerConnectionRecord, 287 ) -> Self { 288 Self::Finalized { 289 workflow_id, 290 connection: Box::new(connection), 291 } 292 } 293 294 pub fn cancelled(workflow: RadrootsNostrSignerPublishWorkflowRecord) -> Self { 295 Self::Cancelled(workflow) 296 } 297 298 pub fn workflow(&self) -> Option<&RadrootsNostrSignerPublishWorkflowRecord> { 299 match self { 300 Self::Begun(workflow) | Self::MarkedPublished(workflow) | Self::Cancelled(workflow) => { 301 Some(workflow) 302 } 303 Self::Finalized { .. } => None, 304 } 305 } 306 307 pub fn finalized_connection(&self) -> Option<&RadrootsNostrSignerConnectionRecord> { 308 match self { 309 Self::Finalized { connection, .. } => Some(connection.as_ref()), 310 _ => None, 311 } 312 } 313 } 314 315 impl RadrootsNostrEmbeddedSignerBackend { 316 pub fn new( 317 manager: RadrootsNostrSignerManager, 318 signer_identity: RadrootsIdentity, 319 ) -> Result<Self, RadrootsNostrSignerError> { 320 let public_identity = signer_identity.to_public(); 321 let existing_identity = manager.signer_identity()?; 322 if let Some(existing_identity) = existing_identity { 323 if !same_public_identity_key(&existing_identity, &public_identity) { 324 return Err(RadrootsNostrSignerError::InvalidState( 325 "embedded signer identity does not match signer manager identity".into(), 326 )); 327 } 328 } else { 329 manager.set_signer_identity(public_identity)?; 330 } 331 332 Ok(Self { 333 manager, 334 signer_identity, 335 }) 336 } 337 338 pub fn new_in_memory( 339 signer_identity: RadrootsIdentity, 340 ) -> Result<Self, RadrootsNostrSignerError> { 341 Self::new(RadrootsNostrSignerManager::new_in_memory(), signer_identity) 342 } 343 344 pub fn manager(&self) -> &RadrootsNostrSignerManager { 345 &self.manager 346 } 347 348 pub fn local_identity(&self) -> &RadrootsIdentity { 349 &self.signer_identity 350 } 351 352 fn local_signer_capability(&self) -> RadrootsNostrLocalSignerCapability { 353 let public_identity = self.signer_identity.to_public(); 354 RadrootsNostrLocalSignerCapability::new( 355 public_identity.id.clone(), 356 public_identity, 357 RadrootsNostrLocalSignerAvailability::SecretBacked, 358 ) 359 } 360 } 361 362 impl RadrootsNostrSignerBackend for RadrootsNostrEmbeddedSignerBackend { 363 fn signer_identity(&self) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> { 364 self.manager.signer_identity() 365 } 366 367 fn set_signer_identity( 368 &self, 369 signer_identity: RadrootsIdentityPublic, 370 ) -> Result<(), RadrootsNostrSignerError> { 371 self.manager.set_signer_identity(signer_identity) 372 } 373 374 fn capabilities( 375 &self, 376 ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> { 377 let mut remote_sessions = Vec::new(); 378 for record in self.manager.list_connections()? { 379 if record.status == RadrootsNostrSignerConnectionStatus::Active { 380 remote_sessions.push(RadrootsNostrRemoteSessionSignerCapability::from(&record)); 381 } 382 } 383 Ok(RadrootsNostrSignerBackendCapabilities::new( 384 Some(self.local_signer_capability()), 385 remote_sessions, 386 )) 387 } 388 389 fn list_connections( 390 &self, 391 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 392 self.manager.list_connections() 393 } 394 395 fn get_connection( 396 &self, 397 connection_id: &RadrootsNostrSignerConnectionId, 398 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 399 self.manager.get_connection(connection_id) 400 } 401 402 fn list_publish_workflows( 403 &self, 404 ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 405 self.manager.list_publish_workflows() 406 } 407 408 fn get_publish_workflow( 409 &self, 410 workflow_id: &RadrootsNostrSignerWorkflowId, 411 ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> { 412 self.manager.get_publish_workflow(workflow_id) 413 } 414 415 fn find_connections_by_client_public_key( 416 &self, 417 client_public_key: &PublicKey, 418 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 419 self.manager 420 .find_connections_by_client_public_key(client_public_key) 421 } 422 423 fn find_connection_by_connect_secret( 424 &self, 425 connect_secret: &str, 426 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 427 self.manager 428 .find_connection_by_connect_secret(connect_secret) 429 } 430 431 fn lookup_session( 432 &self, 433 client_public_key: &PublicKey, 434 connect_secret: Option<&str>, 435 ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> { 436 self.manager 437 .lookup_session(client_public_key, connect_secret) 438 } 439 440 fn evaluate_connect_request( 441 &self, 442 client_public_key: PublicKey, 443 request: RadrootsNostrConnectRequest, 444 ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> { 445 self.manager 446 .evaluate_connect_request(client_public_key, request) 447 } 448 449 fn register_connection( 450 &self, 451 draft: RadrootsNostrSignerConnectionDraft, 452 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 453 self.manager.register_connection(draft) 454 } 455 456 fn set_granted_permissions( 457 &self, 458 connection_id: &RadrootsNostrSignerConnectionId, 459 granted_permissions: RadrootsNostrConnectPermissions, 460 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 461 self.manager 462 .set_granted_permissions(connection_id, granted_permissions) 463 } 464 465 fn approve_connection( 466 &self, 467 connection_id: &RadrootsNostrSignerConnectionId, 468 granted_permissions: RadrootsNostrConnectPermissions, 469 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 470 self.manager 471 .approve_connection(connection_id, granted_permissions) 472 } 473 474 fn reject_connection( 475 &self, 476 connection_id: &RadrootsNostrSignerConnectionId, 477 reason: Option<String>, 478 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 479 self.manager.reject_connection(connection_id, reason) 480 } 481 482 fn revoke_connection( 483 &self, 484 connection_id: &RadrootsNostrSignerConnectionId, 485 reason: Option<String>, 486 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 487 self.manager.revoke_connection(connection_id, reason) 488 } 489 490 fn update_relays( 491 &self, 492 connection_id: &RadrootsNostrSignerConnectionId, 493 relays: Vec<RelayUrl>, 494 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 495 self.manager.update_relays(connection_id, relays) 496 } 497 498 fn require_auth_challenge( 499 &self, 500 connection_id: &RadrootsNostrSignerConnectionId, 501 auth_url: &str, 502 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 503 self.manager.require_auth_challenge(connection_id, auth_url) 504 } 505 506 fn set_pending_request( 507 &self, 508 connection_id: &RadrootsNostrSignerConnectionId, 509 request_message: RadrootsNostrConnectRequestMessage, 510 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 511 self.manager 512 .set_pending_request(connection_id, request_message) 513 } 514 515 fn authorize_auth_challenge( 516 &self, 517 connection_id: &RadrootsNostrSignerConnectionId, 518 ) -> Result<RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> { 519 self.manager.authorize_auth_challenge(connection_id) 520 } 521 522 fn restore_pending_auth_challenge( 523 &self, 524 connection_id: &RadrootsNostrSignerConnectionId, 525 pending_request: RadrootsNostrSignerPendingRequest, 526 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 527 self.manager 528 .restore_pending_auth_challenge(connection_id, pending_request) 529 } 530 531 fn begin_connect_secret_publish_finalization( 532 &self, 533 connection_id: &RadrootsNostrSignerConnectionId, 534 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 535 let workflow = self 536 .manager 537 .begin_connect_secret_publish_finalization(connection_id)?; 538 Ok(RadrootsNostrSignerPublishTransition::begun(workflow)) 539 } 540 541 fn begin_auth_replay_publish_finalization( 542 &self, 543 connection_id: &RadrootsNostrSignerConnectionId, 544 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 545 let workflow = self 546 .manager 547 .begin_auth_replay_publish_finalization(connection_id)?; 548 Ok(RadrootsNostrSignerPublishTransition::begun(workflow)) 549 } 550 551 fn mark_publish_workflow_published( 552 &self, 553 workflow_id: &RadrootsNostrSignerWorkflowId, 554 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 555 let workflow = self.manager.mark_publish_workflow_published(workflow_id)?; 556 Ok(RadrootsNostrSignerPublishTransition::marked_published( 557 workflow, 558 )) 559 } 560 561 fn finalize_publish_workflow( 562 &self, 563 workflow_id: &RadrootsNostrSignerWorkflowId, 564 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 565 let connection = self.manager.finalize_publish_workflow(workflow_id)?; 566 Ok(RadrootsNostrSignerPublishTransition::finalized( 567 workflow_id.clone(), 568 connection, 569 )) 570 } 571 572 fn cancel_publish_workflow( 573 &self, 574 workflow_id: &RadrootsNostrSignerWorkflowId, 575 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 576 let workflow = self.manager.cancel_publish_workflow(workflow_id)?; 577 Ok(RadrootsNostrSignerPublishTransition::cancelled(workflow)) 578 } 579 580 fn mark_authenticated( 581 &self, 582 connection_id: &RadrootsNostrSignerConnectionId, 583 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 584 self.manager.mark_authenticated(connection_id) 585 } 586 587 fn mark_connect_secret_consumed( 588 &self, 589 connection_id: &RadrootsNostrSignerConnectionId, 590 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 591 self.manager.mark_connect_secret_consumed(connection_id) 592 } 593 594 fn evaluate_request( 595 &self, 596 connection_id: &RadrootsNostrSignerConnectionId, 597 request_message: RadrootsNostrConnectRequestMessage, 598 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 599 self.manager 600 .evaluate_request(connection_id, request_message) 601 } 602 603 fn evaluate_auth_replay_publish_workflow( 604 &self, 605 workflow_id: &RadrootsNostrSignerWorkflowId, 606 ) -> Result<RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> { 607 self.manager 608 .evaluate_auth_replay_publish_workflow(workflow_id) 609 } 610 611 fn record_request( 612 &self, 613 connection_id: &RadrootsNostrSignerConnectionId, 614 request_id: &str, 615 method: RadrootsNostrConnectMethod, 616 decision: RadrootsNostrSignerRequestDecision, 617 message: Option<String>, 618 ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> { 619 self.manager 620 .record_request(connection_id, request_id, method, decision, message) 621 } 622 623 fn sign_unsigned_event( 624 &self, 625 unsigned_event: UnsignedEvent, 626 ) -> Result<RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> { 627 let event = unsigned_event.sign_with_keys(self.signer_identity.keys())?; 628 Ok(RadrootsNostrSignerSignOutput::new( 629 RadrootsNostrSignerCapability::LocalAccount(Box::new(self.local_signer_capability())), 630 event, 631 )) 632 } 633 } 634 635 fn same_public_identity_key(left: &RadrootsIdentityPublic, right: &RadrootsIdentityPublic) -> bool { 636 left.id == right.id 637 && left.public_key_hex == right.public_key_hex 638 && left.public_key_npub == right.public_key_npub 639 } 640 641 fn parse_identity_public_key( 642 identity: &RadrootsIdentityPublic, 643 ) -> Result<PublicKey, RadrootsNostrSignerError> { 644 PublicKey::parse(identity.public_key_hex.as_str()) 645 .or_else(|_| PublicKey::from_hex(identity.public_key_hex.as_str())) 646 .map_err(|_| { 647 RadrootsNostrSignerError::InvalidState("identity public key is invalid".into()) 648 }) 649 } 650 651 #[cfg(test)] 652 #[cfg_attr(coverage_nightly, coverage(off))] 653 mod tests { 654 use super::{ 655 RadrootsNostrEmbeddedSignerBackend, RadrootsNostrSignerBackend, 656 RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerPublishTransition, 657 parse_identity_public_key, same_public_identity_key, 658 }; 659 use crate::error::RadrootsNostrSignerError; 660 use crate::evaluation::{ 661 RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerConnectProposal, 662 RadrootsNostrSignerRequestAction, RadrootsNostrSignerSessionLookup, 663 }; 664 use crate::manager::RadrootsNostrSignerManager; 665 use crate::model::{ 666 RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerConnectionDraft, 667 RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerConnectionStatus, 668 RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerRequestDecision, 669 RadrootsNostrSignerStoreState, RadrootsNostrSignerWorkflowId, 670 }; 671 use crate::store::RadrootsNostrSignerStore; 672 use crate::test_support::{ 673 fixture_bob_identity, primary_relay, secondary_relay, synthetic_public_identity, 674 synthetic_public_key, synthetic_secret_hex, 675 }; 676 use nostr::{EventBuilder, EventId, Kind}; 677 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; 678 use radroots_nostr_connect::prelude::{ 679 RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectRequest, 680 RadrootsNostrConnectRequestMessage, 681 }; 682 use serde_json::json; 683 use std::panic::{AssertUnwindSafe, catch_unwind}; 684 use std::sync::Arc; 685 use std::sync::RwLock; 686 use std::sync::atomic::{AtomicU8, Ordering}; 687 688 fn embedded_identity(index: u32) -> RadrootsIdentity { 689 RadrootsIdentity::from_secret_key_str(synthetic_secret_hex(index).as_str()) 690 .expect("identity") 691 } 692 693 fn expect_registration_required( 694 evaluation: RadrootsNostrSignerConnectEvaluation, 695 ) -> RadrootsNostrSignerConnectProposal { 696 match evaluation { 697 RadrootsNostrSignerConnectEvaluation::RegistrationRequired(proposal) => proposal, 698 other => panic!("unexpected connect evaluation: {other:?}"), 699 } 700 } 701 702 fn expect_lookup_connection( 703 lookup: RadrootsNostrSignerSessionLookup, 704 ) -> RadrootsNostrSignerConnectionRecord { 705 match lookup { 706 RadrootsNostrSignerSessionLookup::Connection(found) => *found, 707 other => panic!("unexpected session lookup: {other:?}"), 708 } 709 } 710 711 fn expect_begun_workflow_id( 712 transition: RadrootsNostrSignerPublishTransition, 713 ) -> RadrootsNostrSignerWorkflowId { 714 match transition { 715 RadrootsNostrSignerPublishTransition::Begun(workflow) => workflow.workflow_id, 716 other => panic!("unexpected begin transition: {other:?}"), 717 } 718 } 719 720 fn expect_finalized_transition( 721 transition: RadrootsNostrSignerPublishTransition, 722 ) -> ( 723 RadrootsNostrSignerWorkflowId, 724 RadrootsNostrSignerConnectionRecord, 725 ) { 726 match transition { 727 RadrootsNostrSignerPublishTransition::Finalized { 728 workflow_id, 729 connection, 730 } => (workflow_id, *connection), 731 other => panic!("unexpected finalize transition: {other:?}"), 732 } 733 } 734 735 struct StubBackend { 736 signer_identity: Option<RadrootsIdentityPublic>, 737 signer_identity_error: Option<&'static str>, 738 sign_error_message: Option<&'static str>, 739 } 740 741 #[derive(Default)] 742 struct ToggleSaveStore { 743 state: RwLock<RadrootsNostrSignerStoreState>, 744 mode: AtomicU8, 745 } 746 747 impl ToggleSaveStore { 748 fn set_mode(&self, mode: u8) { 749 self.mode.store(mode, Ordering::SeqCst); 750 } 751 } 752 753 impl RadrootsNostrSignerStore for ToggleSaveStore { 754 fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> { 755 let guard = self.state.read().map_err(|_| { 756 RadrootsNostrSignerError::Store("toggle store lock poisoned".into()) 757 })?; 758 Ok(guard.clone()) 759 } 760 761 fn save( 762 &self, 763 state: &RadrootsNostrSignerStoreState, 764 ) -> Result<(), RadrootsNostrSignerError> { 765 match self.mode.load(Ordering::SeqCst) { 766 1 => Err(RadrootsNostrSignerError::Store("save failed".into())), 767 2 => panic!("toggle save panic"), 768 _ => { 769 let mut guard = self.state.write().map_err(|_| { 770 RadrootsNostrSignerError::Store("toggle store lock poisoned".into()) 771 })?; 772 *guard = state.clone(); 773 Ok(()) 774 } 775 } 776 } 777 } 778 779 impl RadrootsNostrSignerBackend for StubBackend { 780 fn signer_identity( 781 &self, 782 ) -> Result<Option<RadrootsIdentityPublic>, RadrootsNostrSignerError> { 783 if let Some(message) = self.signer_identity_error { 784 return Err(RadrootsNostrSignerError::InvalidState(message.into())); 785 } 786 Ok(self.signer_identity.clone()) 787 } 788 789 fn set_signer_identity( 790 &self, 791 _signer_identity: RadrootsIdentityPublic, 792 ) -> Result<(), RadrootsNostrSignerError> { 793 unreachable!("set_signer_identity not used in tests") 794 } 795 796 fn capabilities( 797 &self, 798 ) -> Result<RadrootsNostrSignerBackendCapabilities, RadrootsNostrSignerError> { 799 unreachable!("capabilities not used in tests") 800 } 801 802 fn list_connections( 803 &self, 804 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 805 unreachable!("list_connections not used in tests") 806 } 807 808 fn get_connection( 809 &self, 810 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 811 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 812 unreachable!("get_connection not used in tests") 813 } 814 815 fn list_publish_workflows( 816 &self, 817 ) -> Result<Vec<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> 818 { 819 unreachable!("list_publish_workflows not used in tests") 820 } 821 822 fn get_publish_workflow( 823 &self, 824 _workflow_id: &RadrootsNostrSignerWorkflowId, 825 ) -> Result<Option<RadrootsNostrSignerPublishWorkflowRecord>, RadrootsNostrSignerError> 826 { 827 unreachable!("get_publish_workflow not used in tests") 828 } 829 830 fn find_connections_by_client_public_key( 831 &self, 832 _client_public_key: &nostr::PublicKey, 833 ) -> Result<Vec<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 834 unreachable!("find_connections_by_client_public_key not used in tests") 835 } 836 837 fn find_connection_by_connect_secret( 838 &self, 839 _connect_secret: &str, 840 ) -> Result<Option<RadrootsNostrSignerConnectionRecord>, RadrootsNostrSignerError> { 841 unreachable!("find_connection_by_connect_secret not used in tests") 842 } 843 844 fn lookup_session( 845 &self, 846 _client_public_key: &nostr::PublicKey, 847 _connect_secret: Option<&str>, 848 ) -> Result<RadrootsNostrSignerSessionLookup, RadrootsNostrSignerError> { 849 unreachable!("lookup_session not used in tests") 850 } 851 852 fn evaluate_connect_request( 853 &self, 854 _client_public_key: nostr::PublicKey, 855 _request: RadrootsNostrConnectRequest, 856 ) -> Result<RadrootsNostrSignerConnectEvaluation, RadrootsNostrSignerError> { 857 unreachable!("evaluate_connect_request not used in tests") 858 } 859 860 fn register_connection( 861 &self, 862 _draft: RadrootsNostrSignerConnectionDraft, 863 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 864 unreachable!("register_connection not used in tests") 865 } 866 867 fn set_granted_permissions( 868 &self, 869 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 870 _granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions, 871 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 872 unreachable!("set_granted_permissions not used in tests") 873 } 874 875 fn approve_connection( 876 &self, 877 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 878 _granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions, 879 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 880 unreachable!("approve_connection not used in tests") 881 } 882 883 fn reject_connection( 884 &self, 885 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 886 _reason: Option<String>, 887 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 888 unreachable!("reject_connection not used in tests") 889 } 890 891 fn revoke_connection( 892 &self, 893 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 894 _reason: Option<String>, 895 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 896 unreachable!("revoke_connection not used in tests") 897 } 898 899 fn update_relays( 900 &self, 901 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 902 _relays: Vec<nostr::RelayUrl>, 903 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 904 unreachable!("update_relays not used in tests") 905 } 906 907 fn require_auth_challenge( 908 &self, 909 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 910 _auth_url: &str, 911 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 912 unreachable!("require_auth_challenge not used in tests") 913 } 914 915 fn set_pending_request( 916 &self, 917 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 918 _request_message: RadrootsNostrConnectRequestMessage, 919 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 920 unreachable!("set_pending_request not used in tests") 921 } 922 923 fn authorize_auth_challenge( 924 &self, 925 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 926 ) -> Result<crate::model::RadrootsNostrSignerAuthorizationOutcome, RadrootsNostrSignerError> 927 { 928 unreachable!("authorize_auth_challenge not used in tests") 929 } 930 931 fn restore_pending_auth_challenge( 932 &self, 933 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 934 _pending_request: crate::model::RadrootsNostrSignerPendingRequest, 935 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 936 unreachable!("restore_pending_auth_challenge not used in tests") 937 } 938 939 fn begin_connect_secret_publish_finalization( 940 &self, 941 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 942 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 943 unreachable!("begin_connect_secret_publish_finalization not used in tests") 944 } 945 946 fn begin_auth_replay_publish_finalization( 947 &self, 948 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 949 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 950 unreachable!("begin_auth_replay_publish_finalization not used in tests") 951 } 952 953 fn mark_publish_workflow_published( 954 &self, 955 _workflow_id: &RadrootsNostrSignerWorkflowId, 956 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 957 unreachable!("mark_publish_workflow_published not used in tests") 958 } 959 960 fn finalize_publish_workflow( 961 &self, 962 _workflow_id: &RadrootsNostrSignerWorkflowId, 963 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 964 unreachable!("finalize_publish_workflow not used in tests") 965 } 966 967 fn cancel_publish_workflow( 968 &self, 969 _workflow_id: &RadrootsNostrSignerWorkflowId, 970 ) -> Result<RadrootsNostrSignerPublishTransition, RadrootsNostrSignerError> { 971 unreachable!("cancel_publish_workflow not used in tests") 972 } 973 974 fn mark_authenticated( 975 &self, 976 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 977 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 978 unreachable!("mark_authenticated not used in tests") 979 } 980 981 fn mark_connect_secret_consumed( 982 &self, 983 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 984 ) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> { 985 unreachable!("mark_connect_secret_consumed not used in tests") 986 } 987 988 fn evaluate_request( 989 &self, 990 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 991 _request_message: RadrootsNostrConnectRequestMessage, 992 ) -> Result<crate::evaluation::RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> 993 { 994 unreachable!("evaluate_request not used in tests") 995 } 996 997 fn evaluate_auth_replay_publish_workflow( 998 &self, 999 _workflow_id: &RadrootsNostrSignerWorkflowId, 1000 ) -> Result<crate::evaluation::RadrootsNostrSignerRequestEvaluation, RadrootsNostrSignerError> 1001 { 1002 unreachable!("evaluate_auth_replay_publish_workflow not used in tests") 1003 } 1004 1005 fn record_request( 1006 &self, 1007 _connection_id: &crate::model::RadrootsNostrSignerConnectionId, 1008 _request_id: &str, 1009 _method: RadrootsNostrConnectMethod, 1010 _decision: RadrootsNostrSignerRequestDecision, 1011 _message: Option<String>, 1012 ) -> Result<crate::model::RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> 1013 { 1014 unreachable!("record_request not used in tests") 1015 } 1016 1017 fn sign_unsigned_event( 1018 &self, 1019 _unsigned_event: nostr::UnsignedEvent, 1020 ) -> Result<super::RadrootsNostrSignerSignOutput, RadrootsNostrSignerError> { 1021 match self.sign_error_message { 1022 Some(message) => Err(RadrootsNostrSignerError::InvalidState(message.into())), 1023 None => unreachable!("sign_unsigned_event success path not used in tests"), 1024 } 1025 } 1026 } 1027 1028 #[test] 1029 fn embedded_backend_bootstraps_signer_identity_and_capabilities() { 1030 let identity = embedded_identity(0x90); 1031 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1032 .expect("embedded backend"); 1033 1034 let signer_identity = backend 1035 .signer_identity() 1036 .expect("signer identity") 1037 .expect("present"); 1038 assert_eq!(signer_identity.id, identity.to_public().id); 1039 1040 let capabilities = backend.capabilities().expect("capabilities"); 1041 let local = capabilities.local_signer.clone().expect("local signer"); 1042 assert_eq!(local.public_identity.id, identity.to_public().id); 1043 assert!(local.is_secret_backed()); 1044 assert!(capabilities.remote_sessions.is_empty()); 1045 assert_eq!(capabilities.all_signers().len(), 1); 1046 let manager_identity = backend 1047 .manager() 1048 .signer_identity() 1049 .expect("manager signer identity") 1050 .expect("stored signer identity"); 1051 assert!(same_public_identity_key( 1052 &manager_identity, 1053 &identity.to_public() 1054 )); 1055 assert_eq!(backend.local_identity().public_key(), identity.public_key()); 1056 } 1057 1058 #[test] 1059 fn embedded_backend_rejects_mismatched_manager_identity() { 1060 let manager = RadrootsNostrSignerManager::new_in_memory(); 1061 manager 1062 .set_signer_identity(fixture_bob_identity()) 1063 .expect("set signer identity"); 1064 1065 let error = RadrootsNostrEmbeddedSignerBackend::new(manager, embedded_identity(0x91)) 1066 .err() 1067 .expect("mismatched identity"); 1068 assert!( 1069 error 1070 .to_string() 1071 .contains("embedded signer identity does not match") 1072 ); 1073 } 1074 1075 #[test] 1076 fn embedded_backend_accepts_matching_manager_identity_and_setter_delegate() { 1077 let identity = embedded_identity(0x97); 1078 let manager = RadrootsNostrSignerManager::new_in_memory(); 1079 let public_identity = identity.to_public(); 1080 manager 1081 .set_signer_identity(public_identity.clone()) 1082 .expect("prime manager identity"); 1083 1084 let backend = RadrootsNostrEmbeddedSignerBackend::new(manager, identity.clone()) 1085 .expect("matching embedded backend"); 1086 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1087 1088 assert_eq!(backend.local_identity().public_key(), identity.public_key()); 1089 assert!(same_public_identity_key( 1090 backend_trait 1091 .signer_identity() 1092 .expect("signer identity") 1093 .as_ref() 1094 .expect("present"), 1095 &public_identity 1096 )); 1097 1098 backend_trait 1099 .set_signer_identity(public_identity.clone()) 1100 .expect("delegate set signer identity"); 1101 let manager_identity = backend 1102 .manager() 1103 .signer_identity() 1104 .expect("manager signer identity") 1105 .expect("stored signer identity"); 1106 assert!(same_public_identity_key( 1107 &manager_identity, 1108 &public_identity 1109 )); 1110 } 1111 1112 #[test] 1113 fn sign_event_builder_propagates_identity_and_sign_errors() { 1114 let missing_identity_backend = StubBackend { 1115 signer_identity: None, 1116 signer_identity_error: None, 1117 sign_error_message: Some("sign should not be called"), 1118 }; 1119 let err = missing_identity_backend 1120 .sign_event_builder(EventBuilder::new(Kind::TextNote, "missing")) 1121 .expect_err("missing identity"); 1122 assert!(matches!( 1123 err, 1124 RadrootsNostrSignerError::MissingSignerIdentity 1125 )); 1126 1127 let identity_error_backend = StubBackend { 1128 signer_identity: None, 1129 signer_identity_error: Some("stub signer identity failure"), 1130 sign_error_message: Some("sign should not be called"), 1131 }; 1132 let err = identity_error_backend 1133 .sign_event_builder(EventBuilder::new(Kind::TextNote, "identity-error")) 1134 .expect_err("signer identity error"); 1135 assert!(err.to_string().contains("stub signer identity failure")); 1136 1137 let mut invalid_identity = synthetic_public_identity(0xaa); 1138 invalid_identity.public_key_hex = "invalid".into(); 1139 let invalid_identity_backend = StubBackend { 1140 signer_identity: Some(invalid_identity), 1141 signer_identity_error: None, 1142 sign_error_message: Some("sign should not be called"), 1143 }; 1144 let err = invalid_identity_backend 1145 .sign_event_builder(EventBuilder::new(Kind::TextNote, "invalid")) 1146 .expect_err("invalid signer identity"); 1147 assert!(err.to_string().contains("identity public key is invalid")); 1148 1149 let signing_error_backend = StubBackend { 1150 signer_identity: Some(synthetic_public_identity(0xab)), 1151 signer_identity_error: None, 1152 sign_error_message: Some("stub sign failure"), 1153 }; 1154 let err = signing_error_backend 1155 .sign_event_builder(EventBuilder::new(Kind::TextNote, "sign-failure")) 1156 .expect_err("sign failure"); 1157 assert!(err.to_string().contains("stub sign failure")); 1158 } 1159 1160 #[test] 1161 fn capabilities_only_include_active_remote_sessions() { 1162 let identity = embedded_identity(0xac); 1163 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1164 .expect("embedded backend"); 1165 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1166 1167 let active = backend_trait 1168 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1169 synthetic_public_key(0xad), 1170 synthetic_public_identity(0xae), 1171 )) 1172 .expect("register active"); 1173 1174 let pending = backend_trait 1175 .register_connection( 1176 RadrootsNostrSignerConnectionDraft::new( 1177 synthetic_public_key(0xaf), 1178 synthetic_public_identity(0xb0), 1179 ) 1180 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser), 1181 ) 1182 .expect("register pending"); 1183 let rejected = backend_trait 1184 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1185 synthetic_public_key(0xb1), 1186 synthetic_public_identity(0xb2), 1187 )) 1188 .expect("register rejected"); 1189 backend_trait 1190 .reject_connection(&rejected.connection_id, Some("rejected".into())) 1191 .expect("reject connection"); 1192 1193 let capabilities = backend_trait.capabilities().expect("capabilities"); 1194 assert_eq!(capabilities.remote_sessions.len(), 1); 1195 assert_eq!( 1196 capabilities.remote_sessions[0].connection_id, 1197 active.connection_id 1198 ); 1199 assert_ne!( 1200 capabilities.remote_sessions[0].connection_id, 1201 pending.connection_id 1202 ); 1203 } 1204 1205 #[test] 1206 fn embedded_backend_propagates_missing_publish_targets() { 1207 let identity = embedded_identity(0xb3); 1208 let backend = 1209 RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity).expect("embedded backend"); 1210 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1211 1212 let missing_connection_id = 1213 crate::model::RadrootsNostrSignerConnectionId::parse("conn-backend-missing") 1214 .expect("connection id"); 1215 let missing_workflow_id = 1216 RadrootsNostrSignerWorkflowId::parse("wf-backend-missing").expect("workflow id"); 1217 1218 assert!( 1219 backend_trait 1220 .begin_connect_secret_publish_finalization(&missing_connection_id) 1221 .expect_err("missing connect workflow") 1222 .to_string() 1223 .contains("connection not found") 1224 ); 1225 assert!( 1226 backend_trait 1227 .begin_auth_replay_publish_finalization(&missing_connection_id) 1228 .expect_err("missing auth workflow") 1229 .to_string() 1230 .contains("connection not found") 1231 ); 1232 assert!( 1233 backend_trait 1234 .mark_publish_workflow_published(&missing_workflow_id) 1235 .expect_err("missing published workflow") 1236 .to_string() 1237 .contains("publish workflow not found") 1238 ); 1239 assert!( 1240 backend_trait 1241 .finalize_publish_workflow(&missing_workflow_id) 1242 .expect_err("missing finalized workflow") 1243 .to_string() 1244 .contains("publish workflow not found") 1245 ); 1246 assert!( 1247 backend_trait 1248 .cancel_publish_workflow(&missing_workflow_id) 1249 .expect_err("missing cancelled workflow") 1250 .to_string() 1251 .contains("publish workflow not found") 1252 ); 1253 } 1254 1255 #[test] 1256 fn embedded_backend_reports_manager_read_and_save_failures() { 1257 let save_fail_store = Arc::new(ToggleSaveStore::default()); 1258 save_fail_store.set_mode(1); 1259 let save_fail_manager = 1260 RadrootsNostrSignerManager::new(save_fail_store).expect("save-fail manager"); 1261 let err = match RadrootsNostrEmbeddedSignerBackend::new( 1262 save_fail_manager, 1263 embedded_identity(0xb4), 1264 ) { 1265 Ok(_) => panic!("expected save failure"), 1266 Err(err) => err, 1267 }; 1268 assert!(err.to_string().contains("save failed")); 1269 1270 let poisoned_store = Arc::new(ToggleSaveStore::default()); 1271 let poisoned_manager = 1272 RadrootsNostrSignerManager::new(poisoned_store.clone()).expect("poison manager"); 1273 let backend = RadrootsNostrEmbeddedSignerBackend::new( 1274 poisoned_manager.clone(), 1275 embedded_identity(0xb5), 1276 ) 1277 .expect("embedded backend"); 1278 poisoned_store.set_mode(2); 1279 assert!( 1280 catch_unwind(AssertUnwindSafe(|| { 1281 let _ = backend 1282 .manager() 1283 .set_signer_identity(fixture_bob_identity()); 1284 })) 1285 .is_err() 1286 ); 1287 1288 let err = backend.capabilities().expect_err("poisoned capabilities"); 1289 assert!(err.to_string().contains("signer state lock poisoned")); 1290 1291 let err = match RadrootsNostrEmbeddedSignerBackend::new( 1292 poisoned_manager, 1293 embedded_identity(0xb5), 1294 ) { 1295 Ok(_) => panic!("expected poisoned new failure"), 1296 Err(err) => err, 1297 }; 1298 assert!(err.to_string().contains("signer state lock poisoned")); 1299 } 1300 1301 #[test] 1302 fn embedded_backend_sign_unsigned_event_rejects_invalid_precomputed_id() { 1303 let identity = embedded_identity(0xb6); 1304 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1305 .expect("embedded backend"); 1306 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1307 1308 let mut unsigned_event = 1309 EventBuilder::new(Kind::TextNote, "hello").build(identity.public_key()); 1310 unsigned_event.id = Some(EventId::all_zeros()); 1311 let err = backend_trait 1312 .sign_unsigned_event(unsigned_event) 1313 .expect_err("invalid precomputed id"); 1314 assert!(err.to_string().starts_with("sign error:")); 1315 } 1316 1317 #[test] 1318 fn embedded_backend_trait_delegates_connect_and_publish_workflow_methods() { 1319 let identity = embedded_identity(0x92); 1320 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1321 .expect("embedded backend"); 1322 let backend: &dyn RadrootsNostrSignerBackend = &backend; 1323 1324 let evaluation = backend 1325 .evaluate_connect_request( 1326 synthetic_public_key(0x93), 1327 RadrootsNostrConnectRequest::Connect { 1328 remote_signer_public_key: identity.public_key(), 1329 secret: Some("connect-secret".into()), 1330 requested_permissions: vec![RadrootsNostrConnectPermission::new( 1331 RadrootsNostrConnectMethod::Ping, 1332 )] 1333 .into(), 1334 }, 1335 ) 1336 .expect("connect evaluation"); 1337 let proposal = expect_registration_required(evaluation); 1338 let connection = backend 1339 .register_connection( 1340 proposal 1341 .into_connection_draft(synthetic_public_identity(0x94)) 1342 .with_relays(vec![primary_relay()]), 1343 ) 1344 .expect("register connection"); 1345 1346 let capabilities = backend.capabilities().expect("capabilities"); 1347 assert_eq!(capabilities.remote_sessions.len(), 1); 1348 1349 let begun = backend 1350 .begin_connect_secret_publish_finalization(&connection.connection_id) 1351 .expect("begin workflow"); 1352 let workflow_id = expect_begun_workflow_id(begun.clone()); 1353 assert_eq!( 1354 begun.workflow().expect("begun workflow").connection_id, 1355 connection.connection_id 1356 ); 1357 1358 let published = backend 1359 .mark_publish_workflow_published(&workflow_id) 1360 .expect("mark published"); 1361 assert!(matches!( 1362 published, 1363 RadrootsNostrSignerPublishTransition::MarkedPublished(_) 1364 )); 1365 1366 let finalized = backend 1367 .finalize_publish_workflow(&workflow_id) 1368 .expect("finalize workflow"); 1369 let (finalized_workflow_id, finalized_connection) = expect_finalized_transition(finalized); 1370 assert_eq!(finalized_workflow_id, workflow_id); 1371 assert!(finalized_connection.connect_secret_is_consumed()); 1372 1373 let audit = backend 1374 .record_request( 1375 &connection.connection_id, 1376 "req-1", 1377 RadrootsNostrConnectMethod::Ping, 1378 RadrootsNostrSignerRequestDecision::Allowed, 1379 None, 1380 ) 1381 .expect("record request"); 1382 assert_eq!(audit.method, RadrootsNostrConnectMethod::Ping); 1383 } 1384 1385 #[test] 1386 fn embedded_backend_delegates_lookup_state_and_auth_workflow_methods() { 1387 let identity = embedded_identity(0xa0); 1388 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1389 .expect("embedded backend"); 1390 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1391 1392 let connect_evaluation = backend_trait 1393 .evaluate_connect_request( 1394 synthetic_public_key(0xa1), 1395 RadrootsNostrConnectRequest::Connect { 1396 remote_signer_public_key: identity.public_key(), 1397 secret: Some("connect-secret-2".into()), 1398 requested_permissions: vec![RadrootsNostrConnectPermission::new( 1399 RadrootsNostrConnectMethod::Ping, 1400 )] 1401 .into(), 1402 }, 1403 ) 1404 .expect("connect evaluation"); 1405 let connect_proposal = expect_registration_required(connect_evaluation); 1406 let connection = backend_trait 1407 .register_connection( 1408 connect_proposal 1409 .into_connection_draft(synthetic_public_identity(0xa2)) 1410 .with_relays(vec![primary_relay()]), 1411 ) 1412 .expect("register connect-secret connection"); 1413 1414 assert_eq!(backend_trait.list_connections().expect("list").len(), 1); 1415 assert_eq!( 1416 backend_trait 1417 .get_connection(&connection.connection_id) 1418 .expect("get connection") 1419 .expect("stored connection") 1420 .connection_id, 1421 connection.connection_id 1422 ); 1423 assert_eq!( 1424 backend_trait 1425 .find_connections_by_client_public_key(&connection.client_public_key) 1426 .expect("find by client key") 1427 .len(), 1428 1 1429 ); 1430 assert_eq!( 1431 backend_trait 1432 .find_connection_by_connect_secret("connect-secret-2") 1433 .expect("find by secret") 1434 .expect("stored by secret") 1435 .connection_id, 1436 connection.connection_id 1437 ); 1438 let looked_up = expect_lookup_connection( 1439 backend_trait 1440 .lookup_session(&connection.client_public_key, Some("connect-secret-2")) 1441 .expect("lookup session"), 1442 ); 1443 assert_eq!(looked_up.connection_id, connection.connection_id); 1444 1445 let with_relays = backend_trait 1446 .update_relays( 1447 &connection.connection_id, 1448 vec![primary_relay(), secondary_relay()], 1449 ) 1450 .expect("update relays"); 1451 assert_eq!(with_relays.relays.len(), 2); 1452 1453 let evaluation = backend_trait 1454 .evaluate_request( 1455 &connection.connection_id, 1456 RadrootsNostrConnectRequestMessage::new( 1457 "req-ping", 1458 RadrootsNostrConnectRequest::Ping, 1459 ), 1460 ) 1461 .expect("evaluate request"); 1462 assert!(matches!( 1463 evaluation.action, 1464 RadrootsNostrSignerRequestAction::Allowed { .. } 1465 )); 1466 1467 let authenticated = backend_trait 1468 .mark_authenticated(&connection.connection_id) 1469 .expect("mark authenticated"); 1470 assert!(authenticated.last_authenticated_at_unix.is_some()); 1471 1472 let pending_connection = backend_trait 1473 .register_connection( 1474 RadrootsNostrSignerConnectionDraft::new( 1475 synthetic_public_key(0xab), 1476 synthetic_public_identity(0xac), 1477 ) 1478 .with_requested_permissions( 1479 vec![RadrootsNostrConnectPermission::new( 1480 RadrootsNostrConnectMethod::Ping, 1481 )] 1482 .into(), 1483 ) 1484 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser), 1485 ) 1486 .expect("register pending connection"); 1487 let granted_permissions: radroots_nostr_connect::prelude::RadrootsNostrConnectPermissions = 1488 vec![RadrootsNostrConnectPermission::new( 1489 RadrootsNostrConnectMethod::Ping, 1490 )] 1491 .into(); 1492 let granted = backend_trait 1493 .set_granted_permissions( 1494 &pending_connection.connection_id, 1495 granted_permissions.clone(), 1496 ) 1497 .expect("set granted permissions"); 1498 assert_eq!(granted.connection_id, pending_connection.connection_id); 1499 1500 let approved = backend_trait 1501 .approve_connection(&pending_connection.connection_id, granted_permissions) 1502 .expect("approve connection"); 1503 assert_eq!(approved.status, RadrootsNostrSignerConnectionStatus::Active); 1504 1505 let begun = backend_trait 1506 .begin_connect_secret_publish_finalization(&connection.connection_id) 1507 .expect("begin connect workflow"); 1508 let workflow = begun.workflow().expect("begun workflow").clone(); 1509 assert!(begun.finalized_connection().is_none()); 1510 assert_eq!( 1511 backend_trait 1512 .list_publish_workflows() 1513 .expect("list publish workflows") 1514 .len(), 1515 1 1516 ); 1517 assert_eq!( 1518 backend_trait 1519 .get_publish_workflow(&workflow.workflow_id) 1520 .expect("get publish workflow") 1521 .expect("stored workflow") 1522 .workflow_id, 1523 workflow.workflow_id 1524 ); 1525 1526 let published = backend_trait 1527 .mark_publish_workflow_published(&workflow.workflow_id) 1528 .expect("mark publish workflow"); 1529 assert_eq!( 1530 published 1531 .workflow() 1532 .expect("published workflow") 1533 .workflow_id, 1534 workflow.workflow_id 1535 ); 1536 1537 let finalized = backend_trait 1538 .finalize_publish_workflow(&workflow.workflow_id) 1539 .expect("finalize workflow"); 1540 assert!(finalized.workflow().is_none()); 1541 assert_eq!( 1542 finalized 1543 .finalized_connection() 1544 .expect("finalized connection") 1545 .connection_id, 1546 connection.connection_id 1547 ); 1548 1549 let audit = backend_trait 1550 .record_request( 1551 &connection.connection_id, 1552 "req-audit", 1553 RadrootsNostrConnectMethod::Ping, 1554 RadrootsNostrSignerRequestDecision::Allowed, 1555 None, 1556 ) 1557 .expect("record request"); 1558 assert_eq!(audit.connection_id, connection.connection_id); 1559 1560 let consumed_connection = backend_trait 1561 .register_connection( 1562 RadrootsNostrSignerConnectionDraft::new( 1563 synthetic_public_key(0xa3), 1564 synthetic_public_identity(0xa4), 1565 ) 1566 .with_connect_secret("manual-secret"), 1567 ) 1568 .expect("register consumed connection"); 1569 let consumed = backend_trait 1570 .mark_connect_secret_consumed(&consumed_connection.connection_id) 1571 .expect("mark connect secret consumed"); 1572 assert!(consumed.connect_secret_is_consumed()); 1573 1574 let rejected = backend_trait 1575 .register_connection(RadrootsNostrSignerConnectionDraft::new( 1576 synthetic_public_key(0xa5), 1577 synthetic_public_identity(0xa6), 1578 )) 1579 .expect("register rejected connection"); 1580 let rejected = backend_trait 1581 .reject_connection(&rejected.connection_id, Some("rejected".into())) 1582 .expect("reject connection"); 1583 assert_eq!( 1584 rejected.status, 1585 RadrootsNostrSignerConnectionStatus::Rejected 1586 ); 1587 1588 let auth_connection = backend_trait 1589 .register_connection( 1590 RadrootsNostrSignerConnectionDraft::new( 1591 synthetic_public_key(0xa7), 1592 synthetic_public_identity(0xa8), 1593 ) 1594 .with_requested_permissions( 1595 vec![RadrootsNostrConnectPermission::new( 1596 RadrootsNostrConnectMethod::Ping, 1597 )] 1598 .into(), 1599 ), 1600 ) 1601 .expect("register auth connection"); 1602 backend_trait 1603 .require_auth_challenge( 1604 &auth_connection.connection_id, 1605 "https://api.example.com/auth", 1606 ) 1607 .expect("require auth challenge"); 1608 let pending = backend_trait 1609 .set_pending_request( 1610 &auth_connection.connection_id, 1611 RadrootsNostrConnectRequestMessage::new( 1612 "req-auth-replay", 1613 RadrootsNostrConnectRequest::Ping, 1614 ), 1615 ) 1616 .expect("set pending request"); 1617 assert!(pending.pending_request.is_some()); 1618 let authorized = backend_trait 1619 .authorize_auth_challenge(&auth_connection.connection_id) 1620 .expect("authorize auth challenge"); 1621 let pending_request = authorized.pending_request.expect("pending request"); 1622 let restored = backend_trait 1623 .restore_pending_auth_challenge(&auth_connection.connection_id, pending_request.clone()) 1624 .expect("restore pending auth challenge"); 1625 assert_eq!(restored.pending_request.as_ref(), Some(&pending_request)); 1626 1627 let auth_workflow = backend_trait 1628 .begin_auth_replay_publish_finalization(&auth_connection.connection_id) 1629 .expect("begin auth replay") 1630 .workflow() 1631 .expect("auth replay workflow") 1632 .clone(); 1633 let replay_evaluation = backend_trait 1634 .evaluate_auth_replay_publish_workflow(&auth_workflow.workflow_id) 1635 .expect("evaluate auth replay workflow"); 1636 assert_eq!( 1637 replay_evaluation.connection.connection_id, 1638 auth_connection.connection_id 1639 ); 1640 let cancelled = backend_trait 1641 .cancel_publish_workflow(&auth_workflow.workflow_id) 1642 .expect("cancel auth workflow"); 1643 assert_eq!( 1644 cancelled 1645 .workflow() 1646 .expect("cancelled workflow") 1647 .workflow_id, 1648 auth_workflow.workflow_id 1649 ); 1650 1651 let revoked = backend_trait 1652 .revoke_connection(&auth_connection.connection_id, Some("revoked".into())) 1653 .expect("revoke connection"); 1654 assert_eq!(revoked.status, RadrootsNostrSignerConnectionStatus::Revoked); 1655 } 1656 1657 #[test] 1658 fn embedded_backend_signs_builder_with_local_capability() { 1659 let identity = embedded_identity(0x95); 1660 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1661 .expect("embedded backend"); 1662 let backend_trait: &dyn RadrootsNostrSignerBackend = &backend; 1663 1664 let output = backend_trait 1665 .sign_event_builder(EventBuilder::new(Kind::TextNote, "hello")) 1666 .expect("sign event builder"); 1667 let direct_output = 1668 <RadrootsNostrEmbeddedSignerBackend as RadrootsNostrSignerBackend>::sign_unsigned_event( 1669 &backend, 1670 EventBuilder::new(Kind::TextNote, "hello-direct").build(identity.public_key()), 1671 ) 1672 .expect("sign unsigned event"); 1673 1674 assert_eq!(output.event.pubkey, identity.public_key()); 1675 assert_eq!(direct_output.event.pubkey, identity.public_key()); 1676 let local = output.signer.local_account().expect("local signer"); 1677 assert_eq!(local.public_identity.id, identity.to_public().id); 1678 assert!(local.is_secret_backed()); 1679 } 1680 1681 #[test] 1682 fn embedded_backend_can_prepare_and_cancel_auth_replay_workflow() { 1683 let identity = embedded_identity(0x96); 1684 let backend = RadrootsNostrEmbeddedSignerBackend::new_in_memory(identity.clone()) 1685 .expect("embedded backend"); 1686 let backend: &dyn RadrootsNostrSignerBackend = &backend; 1687 1688 let connection = backend 1689 .register_connection( 1690 RadrootsNostrSignerConnectionDraft::new( 1691 synthetic_public_key(0x97), 1692 synthetic_public_identity(0x98), 1693 ) 1694 .with_requested_permissions( 1695 vec![RadrootsNostrConnectPermission::new( 1696 RadrootsNostrConnectMethod::Ping, 1697 )] 1698 .into(), 1699 ), 1700 ) 1701 .expect("register connection"); 1702 backend 1703 .require_auth_challenge(&connection.connection_id, "https://api.example.com/auth") 1704 .expect("require auth"); 1705 backend 1706 .set_pending_request( 1707 &connection.connection_id, 1708 RadrootsNostrConnectRequestMessage::new( 1709 "req-auth", 1710 RadrootsNostrConnectRequest::Ping, 1711 ), 1712 ) 1713 .expect("set pending request"); 1714 1715 let begun = backend 1716 .begin_auth_replay_publish_finalization(&connection.connection_id) 1717 .expect("begin auth replay"); 1718 let workflow_id = begun 1719 .workflow() 1720 .expect("begun auth replay workflow") 1721 .workflow_id 1722 .clone(); 1723 1724 let cancelled = backend 1725 .cancel_publish_workflow(&workflow_id) 1726 .expect("cancel workflow"); 1727 assert!(matches!( 1728 cancelled, 1729 RadrootsNostrSignerPublishTransition::Cancelled(_) 1730 )); 1731 } 1732 1733 #[test] 1734 fn backend_capabilities_all_signers_supports_remote_only_and_identity_helpers() { 1735 let remote = crate::capability::RadrootsNostrRemoteSessionSignerCapability::new( 1736 crate::model::RadrootsNostrSignerConnectionId::new_v7(), 1737 synthetic_public_identity(0xb0), 1738 synthetic_public_identity(0xb1), 1739 ); 1740 let capabilities = RadrootsNostrSignerBackendCapabilities::new(None, vec![remote.clone()]); 1741 1742 assert_eq!( 1743 capabilities.all_signers(), 1744 vec![ 1745 crate::capability::RadrootsNostrSignerCapability::RemoteSession(Box::new(remote,)) 1746 ] 1747 ); 1748 1749 let valid_identity = synthetic_public_identity(0xb2); 1750 assert!(same_public_identity_key(&valid_identity, &valid_identity)); 1751 assert_eq!( 1752 parse_identity_public_key(&valid_identity).expect("valid public key"), 1753 synthetic_public_key(0xb2) 1754 ); 1755 let mut valid_identity_with_different_hex = valid_identity.clone(); 1756 valid_identity_with_different_hex.public_key_hex = 1757 synthetic_public_identity(0xb3).public_key_hex; 1758 assert!(!same_public_identity_key( 1759 &valid_identity, 1760 &valid_identity_with_different_hex 1761 )); 1762 1763 let invalid_identity: RadrootsIdentityPublic = serde_json::from_value(json!({ 1764 "id": "not-a-public-key", 1765 "public_key_hex": "not-a-public-key", 1766 "public_key_npub": "npub1invalid" 1767 })) 1768 .expect("invalid identity payload"); 1769 let error = parse_identity_public_key(&invalid_identity) 1770 .err() 1771 .expect("invalid public identity"); 1772 assert!(error.to_string().contains("identity public key is invalid")); 1773 } 1774 1775 #[test] 1776 fn backend_test_helpers_reject_unexpected_variants() { 1777 let connection = RadrootsNostrSignerConnectionRecord::new( 1778 crate::model::RadrootsNostrSignerConnectionId::new_v7(), 1779 synthetic_public_identity(0xb4), 1780 RadrootsNostrSignerConnectionDraft::new( 1781 synthetic_public_key(0xb5), 1782 synthetic_public_identity(0xb6), 1783 ), 1784 1, 1785 ); 1786 let workflow = RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization( 1787 connection.connection_id.clone(), 1788 1, 1789 ); 1790 1791 assert!( 1792 std::panic::catch_unwind(|| { 1793 expect_registration_required( 1794 RadrootsNostrSignerConnectEvaluation::ExistingConnection(Box::new( 1795 connection.clone(), 1796 )), 1797 ) 1798 }) 1799 .is_err() 1800 ); 1801 assert!( 1802 std::panic::catch_unwind(|| { 1803 expect_lookup_connection(RadrootsNostrSignerSessionLookup::None) 1804 }) 1805 .is_err() 1806 ); 1807 assert!( 1808 std::panic::catch_unwind(|| { 1809 expect_begun_workflow_id(RadrootsNostrSignerPublishTransition::cancelled( 1810 workflow.clone(), 1811 )) 1812 }) 1813 .is_err() 1814 ); 1815 assert!( 1816 std::panic::catch_unwind(|| { 1817 expect_finalized_transition(RadrootsNostrSignerPublishTransition::begun(workflow)) 1818 }) 1819 .is_err() 1820 ); 1821 } 1822 }