protocol.rs (30683B)
1 use crate::error::RadrootsAppRemoteSignerError; 2 use crate::input::{RadrootsAppRemoteSignerTarget, radroots_app_remote_signer_preview}; 3 use crate::session::RadrootsAppRemoteSignerSessionRecord; 4 use nostr::JsonUtil; 5 use nostr::{EventBuilder, RelayUrl, UnsignedEvent}; 6 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; 7 use radroots_nostr::prelude::{ 8 RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, 9 RadrootsNostrRelayPoolNotification, RadrootsNostrTimestamp, radroots_nostr_filter_tag, 10 }; 11 use radroots_nostr_connect::prelude::{ 12 RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientProgress, 13 RadrootsNostrConnectClientRequest, RadrootsNostrConnectClientTarget, 14 RadrootsNostrConnectClientTransport, RadrootsNostrConnectClientTransportFuture, 15 RadrootsNostrConnectError, RadrootsNostrConnectMethod, 16 RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectPermissions, 17 RadrootsNostrConnectRequest, RadrootsNostrConnectResponse, execute_request_with_transport, 18 }; 19 use std::sync::atomic::{AtomicU64, Ordering}; 20 use std::time::Duration; 21 use tokio::sync::broadcast; 22 use tokio::time::timeout; 23 24 const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); 25 const GET_SESSION_CAPABILITY_TIMEOUT: Duration = Duration::from_secs(60); 26 const SWITCH_RELAYS_TIMEOUT: Duration = Duration::from_secs(30); 27 const SIGN_EVENT_TIMEOUT: Duration = Duration::from_secs(60); 28 static REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1); 29 30 #[derive(Debug, Clone)] 31 pub struct RadrootsAppRemoteSignerPendingSession { 32 pub record: RadrootsAppRemoteSignerSessionRecord, 33 pub client_secret_key_hex: String, 34 } 35 36 #[derive(Debug, Clone)] 37 pub struct RadrootsAppRemoteSignerApprovedSession { 38 pub user_identity: RadrootsIdentityPublic, 39 pub relays: Vec<String>, 40 pub approved_permissions: RadrootsNostrConnectPermissions, 41 } 42 43 #[derive(Debug, Clone, PartialEq, Eq)] 44 pub struct RadrootsAppRemoteSignerSignedEvent { 45 pub event_id_hex: String, 46 pub event_json: String, 47 pub relays: Vec<String>, 48 } 49 50 #[derive(Debug, Clone, PartialEq, Eq)] 51 pub enum RadrootsAppRemoteSignerProgressUpdate { 52 AuthChallenge { url: String }, 53 } 54 55 #[derive(Debug, Clone)] 56 pub enum RadrootsAppRemoteSignerPendingPollOutcome { 57 PendingApproval, 58 Approved(RadrootsAppRemoteSignerApprovedSession), 59 TransportFailure { message: String }, 60 Rejected { message: String }, 61 FatalError { message: String }, 62 } 63 64 pub(crate) struct RadrootsAppRemoteSignerPendingPoller { 65 client: ConnectedRemoteSignerSessionClient, 66 } 67 68 struct ConnectedRemoteSignerSessionClient { 69 client_identity: RadrootsIdentity, 70 target: RadrootsAppRemoteSignerTarget, 71 client_target: RadrootsNostrConnectClientTarget, 72 transport: ConnectedRemoteSignerTransport, 73 } 74 75 struct ConnectedRemoteSignerTransport { 76 client: RadrootsNostrClient, 77 notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>, 78 } 79 80 pub async fn radroots_app_remote_signer_connect_pending( 81 input: &str, 82 ) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { 83 let target = radroots_app_remote_signer_preview(input)?; 84 connect_pending_session(target).await 85 } 86 87 pub async fn radroots_app_remote_signer_poll_pending_session( 88 record: &RadrootsAppRemoteSignerSessionRecord, 89 client_secret_key_hex: &str, 90 ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> { 91 radroots_app_remote_signer_poll_pending_session_with_progress( 92 record, 93 client_secret_key_hex, 94 |_| {}, 95 ) 96 .await 97 } 98 99 pub async fn radroots_app_remote_signer_poll_pending_session_with_progress<F>( 100 record: &RadrootsAppRemoteSignerSessionRecord, 101 client_secret_key_hex: &str, 102 mut progress: F, 103 ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> 104 where 105 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 106 { 107 let mut poller = 108 radroots_app_remote_signer_open_pending_poller(record, client_secret_key_hex).await?; 109 radroots_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress).await 110 } 111 112 pub(crate) async fn radroots_app_remote_signer_open_pending_poller( 113 record: &RadrootsAppRemoteSignerSessionRecord, 114 client_secret_key_hex: &str, 115 ) -> Result<RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerError> { 116 let client_identity = load_client_identity(client_secret_key_hex)?; 117 let target = target_for_record(record); 118 Ok(RadrootsAppRemoteSignerPendingPoller { 119 client: ConnectedRemoteSignerSessionClient::connect(client_identity, target).await?, 120 }) 121 } 122 123 pub(crate) async fn radroots_app_remote_signer_poll_pending_poller_with_progress<F>( 124 poller: &mut RadrootsAppRemoteSignerPendingPoller, 125 progress: &mut F, 126 ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> 127 where 128 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 129 { 130 poller.poll_with_progress(progress).await 131 } 132 133 pub async fn radroots_app_remote_signer_sign_kind1_note( 134 record: &RadrootsAppRemoteSignerSessionRecord, 135 client_secret_key_hex: &str, 136 content: &str, 137 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> { 138 radroots_app_remote_signer_sign_kind1_note_with_progress( 139 record, 140 client_secret_key_hex, 141 content, 142 |_| {}, 143 ) 144 .await 145 } 146 147 pub async fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>( 148 record: &RadrootsAppRemoteSignerSessionRecord, 149 client_secret_key_hex: &str, 150 content: &str, 151 mut progress: F, 152 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> 153 where 154 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 155 { 156 sign_kind1_note(record, client_secret_key_hex, content, &mut progress).await 157 } 158 159 pub async fn radroots_app_remote_signer_sign_unsigned_event( 160 record: &RadrootsAppRemoteSignerSessionRecord, 161 client_secret_key_hex: &str, 162 unsigned_event: UnsignedEvent, 163 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> { 164 radroots_app_remote_signer_sign_unsigned_event_with_progress( 165 record, 166 client_secret_key_hex, 167 unsigned_event, 168 |_| {}, 169 ) 170 .await 171 } 172 173 pub async fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>( 174 record: &RadrootsAppRemoteSignerSessionRecord, 175 client_secret_key_hex: &str, 176 unsigned_event: UnsignedEvent, 177 mut progress: F, 178 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> 179 where 180 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 181 { 182 sign_unsigned_event(record, client_secret_key_hex, unsigned_event, &mut progress).await 183 } 184 185 async fn connect_pending_session( 186 target: RadrootsAppRemoteSignerTarget, 187 ) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { 188 let client_identity = RadrootsIdentity::generate(); 189 let connect_request = connect_request_for_target(&target); 190 let response = execute_request( 191 &client_identity, 192 &target, 193 RadrootsNostrConnectMethod::Connect, 194 connect_request, 195 CONNECT_TIMEOUT, 196 ) 197 .await?; 198 199 match response { 200 RadrootsNostrConnectResponse::ConnectAcknowledged 201 | RadrootsNostrConnectResponse::ConnectSecretEcho(_) => { 202 Ok(RadrootsAppRemoteSignerPendingSession { 203 record: RadrootsAppRemoteSignerSessionRecord::pending( 204 client_identity.to_public(), 205 target.signer_identity, 206 target.relays, 207 ), 208 client_secret_key_hex: client_identity.secret_key_hex(), 209 }) 210 } 211 other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { 212 method: RadrootsNostrConnectMethod::Connect, 213 response: format!("{other:?}"), 214 }), 215 } 216 } 217 218 fn connect_request_for_target( 219 target: &RadrootsAppRemoteSignerTarget, 220 ) -> RadrootsNostrConnectRequest { 221 RadrootsNostrConnectRequest::Connect { 222 remote_signer_public_key: parse_public_key_hex( 223 target.signer_identity.public_key_hex.as_str(), 224 ) 225 .expect("signer public key is derived from a validated identity"), 226 secret: target.connect_secret.clone(), 227 requested_permissions: target.requested_permissions.clone(), 228 } 229 } 230 231 async fn sign_kind1_note<F>( 232 record: &RadrootsAppRemoteSignerSessionRecord, 233 client_secret_key_hex: &str, 234 content: &str, 235 progress: &mut F, 236 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> 237 where 238 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 239 { 240 if !record.allows_sign_event_kind1() { 241 return Err(RadrootsAppRemoteSignerError::ConnectFailed( 242 "remote signer has not approved sign_event:kind:1".to_owned(), 243 )); 244 } 245 let user_identity = record.user_identity.as_ref().ok_or_else(|| { 246 RadrootsAppRemoteSignerError::ConnectFailed( 247 "remote signer session is missing the approved user identity".to_owned(), 248 ) 249 })?; 250 let unsigned_event = EventBuilder::text_note(content.trim()) 251 .build(parse_public_key_hex(user_identity.public_key_hex.as_str())?); 252 sign_unsigned_event(record, client_secret_key_hex, unsigned_event, progress).await 253 } 254 255 async fn sign_unsigned_event<F>( 256 record: &RadrootsAppRemoteSignerSessionRecord, 257 client_secret_key_hex: &str, 258 unsigned_event: UnsignedEvent, 259 progress: &mut F, 260 ) -> Result<RadrootsAppRemoteSignerSignedEvent, RadrootsAppRemoteSignerError> 261 where 262 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 263 { 264 let client_identity = load_client_identity(client_secret_key_hex)?; 265 let target = target_for_record(record); 266 let mut client = ConnectedRemoteSignerSessionClient::connect(client_identity, target).await?; 267 let relays = client.sync_relays_if_allowed(record, progress).await?; 268 let response = client 269 .execute_request_with_progress( 270 RadrootsNostrConnectMethod::SignEvent, 271 RadrootsNostrConnectRequest::SignEvent(unsigned_event), 272 SIGN_EVENT_TIMEOUT, 273 progress, 274 ) 275 .await?; 276 277 match response { 278 RadrootsNostrConnectResponse::SignedEvent(event) => { 279 Ok(RadrootsAppRemoteSignerSignedEvent { 280 event_id_hex: event.id.to_hex(), 281 event_json: event.as_json(), 282 relays, 283 }) 284 } 285 RadrootsNostrConnectResponse::Error { error, .. } => { 286 Err(RadrootsAppRemoteSignerError::ConnectFailed(error)) 287 } 288 other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { 289 method: RadrootsNostrConnectMethod::SignEvent, 290 response: format!("{other:?}"), 291 }), 292 } 293 } 294 295 async fn execute_request( 296 client_identity: &RadrootsIdentity, 297 target: &RadrootsAppRemoteSignerTarget, 298 method: RadrootsNostrConnectMethod, 299 request: RadrootsNostrConnectRequest, 300 request_timeout: Duration, 301 ) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> { 302 let mut client = 303 ConnectedRemoteSignerSessionClient::connect(client_identity.clone(), target.clone()) 304 .await?; 305 client 306 .execute_request_with_progress(method, request, request_timeout, &mut |_| {}) 307 .await 308 } 309 310 impl RadrootsAppRemoteSignerPendingPoller { 311 async fn poll_with_progress<F>( 312 &mut self, 313 progress: &mut F, 314 ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> 315 where 316 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 317 { 318 match self 319 .client 320 .execute_request_with_progress( 321 RadrootsNostrConnectMethod::GetSessionCapability, 322 RadrootsNostrConnectRequest::GetSessionCapability, 323 GET_SESSION_CAPABILITY_TIMEOUT, 324 progress, 325 ) 326 .await 327 { 328 Ok(response) => Ok(classify_pending_poll_response(response)), 329 Err(error) => Ok(classify_pending_poll_error(error)), 330 } 331 } 332 } 333 334 impl ConnectedRemoteSignerSessionClient { 335 async fn connect( 336 client_identity: RadrootsIdentity, 337 target: RadrootsAppRemoteSignerTarget, 338 ) -> Result<Self, RadrootsAppRemoteSignerError> { 339 let client_target = client_target_for_app_target(&target)?; 340 let client = RadrootsNostrClient::from_identity(&client_identity); 341 for relay in &target.relays { 342 client 343 .add_relay(relay) 344 .await 345 .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; 346 } 347 client.connect().await; 348 let filter = radroots_nostr_filter_tag( 349 RadrootsNostrFilter::new() 350 .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) 351 .since(RadrootsNostrTimestamp::now()), 352 "p", 353 vec![client_identity.public_key_hex()], 354 ) 355 .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; 356 let notifications = client.notifications(); 357 client 358 .subscribe(filter, None) 359 .await 360 .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; 361 362 Ok(Self { 363 client_identity, 364 target, 365 client_target, 366 transport: ConnectedRemoteSignerTransport { 367 client, 368 notifications, 369 }, 370 }) 371 } 372 373 async fn sync_relays_if_allowed<F>( 374 &mut self, 375 record: &RadrootsAppRemoteSignerSessionRecord, 376 progress: &mut F, 377 ) -> Result<Vec<String>, RadrootsAppRemoteSignerError> 378 where 379 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 380 { 381 if !record.allows_switch_relays() { 382 return Ok(self.target.relays.clone()); 383 } 384 385 match self 386 .execute_request_with_progress( 387 RadrootsNostrConnectMethod::SwitchRelays, 388 RadrootsNostrConnectRequest::SwitchRelays, 389 SWITCH_RELAYS_TIMEOUT, 390 progress, 391 ) 392 .await? 393 { 394 RadrootsNostrConnectResponse::RelayList(relays) => { 395 let relays: Vec<String> = relays.iter().map(ToString::to_string).collect(); 396 self.client_target.relays = relays 397 .iter() 398 .map(|relay| parse_relay_url(relay)) 399 .collect::<Result<Vec<_>, _>>()?; 400 self.target.relays = relays.clone(); 401 Ok(relays) 402 } 403 RadrootsNostrConnectResponse::RelayListUnchanged => Ok(self.target.relays.clone()), 404 RadrootsNostrConnectResponse::Error { error, .. } => { 405 Err(RadrootsAppRemoteSignerError::ConnectFailed(format!( 406 "remote signer rejected relay update: {error}" 407 ))) 408 } 409 other => Err(RadrootsAppRemoteSignerError::UnexpectedResponse { 410 method: RadrootsNostrConnectMethod::SwitchRelays, 411 response: format!("{other:?}"), 412 }), 413 } 414 } 415 416 async fn execute_request_with_progress<F>( 417 &mut self, 418 method: RadrootsNostrConnectMethod, 419 request: RadrootsNostrConnectRequest, 420 request_timeout: Duration, 421 progress: &mut F, 422 ) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> 423 where 424 F: FnMut(RadrootsAppRemoteSignerProgressUpdate), 425 { 426 let request_id = next_request_id(method.to_string().as_str()); 427 let response_method = method.clone(); 428 let client_keys = self.client_identity.keys().clone(); 429 let client_target = self.client_target.clone(); 430 let request = RadrootsNostrConnectClientRequest::new(request_id, request); 431 let response = timeout( 432 request_timeout, 433 execute_request_with_transport( 434 &client_keys, 435 &client_target, 436 request, 437 &mut self.transport, 438 |event| { 439 match event { 440 RadrootsNostrConnectClientProgress::AuthChallenge { url } => { 441 progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url }); 442 } 443 } 444 Ok(()) 445 }, 446 ), 447 ) 448 .await 449 .map_err(|_| RadrootsAppRemoteSignerError::RequestTimedOut { 450 method: response_method.clone(), 451 })?; 452 response.map_err(|error| app_error_from_nostr_connect_error(&response_method, error)) 453 } 454 } 455 456 impl RadrootsNostrConnectClientTransport for ConnectedRemoteSignerTransport { 457 fn publish_request_event<'a>( 458 &'a mut self, 459 event: RadrootsNostrEvent, 460 ) -> RadrootsNostrConnectClientTransportFuture<'a, ()> { 461 Box::pin(async move { 462 self.client 463 .send_event(&event) 464 .await 465 .map(|_| ()) 466 .map_err(|error| RadrootsNostrConnectError::Transport { 467 reason: error.to_string(), 468 }) 469 }) 470 } 471 472 fn next_response_event<'a>( 473 &'a mut self, 474 ) -> RadrootsNostrConnectClientTransportFuture<'a, RadrootsNostrEvent> { 475 Box::pin(async move { 476 loop { 477 let notification = match self.notifications.recv().await { 478 Ok(notification) => notification, 479 Err(broadcast::error::RecvError::Lagged(_)) => continue, 480 Err(broadcast::error::RecvError::Closed) => { 481 return Err(RadrootsNostrConnectError::Transport { 482 reason: "remote signer notification stream closed".to_owned(), 483 }); 484 } 485 }; 486 let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { 487 continue; 488 }; 489 let event = *event; 490 if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) { 491 continue; 492 } 493 return Ok(event); 494 } 495 }) 496 } 497 } 498 499 fn client_target_for_app_target( 500 target: &RadrootsAppRemoteSignerTarget, 501 ) -> Result<RadrootsNostrConnectClientTarget, RadrootsAppRemoteSignerError> { 502 Ok(RadrootsNostrConnectClientTarget::new( 503 parse_public_key_hex(target.signer_identity.public_key_hex.as_str())?, 504 target 505 .relays 506 .iter() 507 .map(|relay| parse_relay_url(relay)) 508 .collect::<Result<Vec<_>, _>>()?, 509 )) 510 } 511 512 fn parse_relay_url(value: &str) -> Result<RelayUrl, RadrootsAppRemoteSignerError> { 513 RelayUrl::parse(value).map_err(|error| { 514 RadrootsAppRemoteSignerError::ConnectFailed(format!( 515 "invalid remote signer relay `{value}`: {error}" 516 )) 517 }) 518 } 519 520 fn app_error_from_nostr_connect_error( 521 method: &RadrootsNostrConnectMethod, 522 error: RadrootsNostrConnectError, 523 ) -> RadrootsAppRemoteSignerError { 524 match error { 525 RadrootsNostrConnectError::RequestTimedOut => { 526 RadrootsAppRemoteSignerError::RequestTimedOut { 527 method: method.clone(), 528 } 529 } 530 RadrootsNostrConnectError::Transport { reason } 531 | RadrootsNostrConnectError::Encrypt { reason } 532 | RadrootsNostrConnectError::Sign { reason } => { 533 RadrootsAppRemoteSignerError::ConnectFailed(reason) 534 } 535 RadrootsNostrConnectError::Decrypt { reason } 536 | RadrootsNostrConnectError::Json(reason) 537 | RadrootsNostrConnectError::InvalidResponsePayload { reason, .. } => { 538 RadrootsAppRemoteSignerError::UnexpectedResponse { 539 method: method.clone(), 540 response: reason, 541 } 542 } 543 other => RadrootsAppRemoteSignerError::UnexpectedResponse { 544 method: method.clone(), 545 response: other.to_string(), 546 }, 547 } 548 } 549 550 fn classify_pending_poll_response( 551 response: RadrootsNostrConnectResponse, 552 ) -> RadrootsAppRemoteSignerPendingPollOutcome { 553 match response.into_pending_connection_poll_outcome() { 554 RadrootsNostrConnectPendingConnectionPollOutcome::Approved(public_key) => { 555 RadrootsAppRemoteSignerPendingPollOutcome::Approved( 556 RadrootsAppRemoteSignerApprovedSession { 557 user_identity: RadrootsIdentityPublic::new(public_key), 558 relays: Vec::new(), 559 approved_permissions: RadrootsNostrConnectPermissions::default(), 560 }, 561 ) 562 } 563 RadrootsNostrConnectPendingConnectionPollOutcome::ApprovedCapability(capability) => { 564 RadrootsAppRemoteSignerPendingPollOutcome::Approved( 565 RadrootsAppRemoteSignerApprovedSession { 566 user_identity: RadrootsIdentityPublic::new(capability.user_public_key), 567 relays: capability 568 .relays 569 .into_iter() 570 .map(|relay| relay.to_string()) 571 .collect(), 572 approved_permissions: capability.permissions, 573 }, 574 ) 575 } 576 RadrootsNostrConnectPendingConnectionPollOutcome::PendingApproval => { 577 RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval 578 } 579 RadrootsNostrConnectPendingConnectionPollOutcome::Rejected { message } => { 580 RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message } 581 } 582 RadrootsNostrConnectPendingConnectionPollOutcome::AuthChallenge { url } => { 583 RadrootsAppRemoteSignerPendingPollOutcome::FatalError { 584 message: format!("unexpected remote signer authorization challenge: {url}"), 585 } 586 } 587 RadrootsNostrConnectPendingConnectionPollOutcome::UnexpectedResponse { response } => { 588 RadrootsAppRemoteSignerPendingPollOutcome::FatalError { 589 message: format!("unexpected remote signer response: {response}"), 590 } 591 } 592 } 593 } 594 595 fn classify_pending_poll_error( 596 error: RadrootsAppRemoteSignerError, 597 ) -> RadrootsAppRemoteSignerPendingPollOutcome { 598 match error { 599 RadrootsAppRemoteSignerError::RequestTimedOut { .. } => { 600 RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { 601 message: "remote signer did not respond yet".to_owned(), 602 } 603 } 604 RadrootsAppRemoteSignerError::ConnectFailed(message) => { 605 RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message } 606 } 607 RadrootsAppRemoteSignerError::UnexpectedResponse { .. } => { 608 RadrootsAppRemoteSignerPendingPollOutcome::FatalError { 609 message: error.to_string(), 610 } 611 } 612 other => RadrootsAppRemoteSignerPendingPollOutcome::FatalError { 613 message: other.to_string(), 614 }, 615 } 616 } 617 618 fn next_request_id(prefix: &str) -> String { 619 let tick = REQUEST_COUNTER.fetch_add(1, Ordering::AcqRel); 620 format!("{prefix}-{tick}") 621 } 622 623 fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsAppRemoteSignerError> { 624 nostr::PublicKey::parse(value) 625 .or_else(|_| nostr::PublicKey::from_hex(value)) 626 .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string())) 627 } 628 629 fn load_client_identity( 630 client_secret_key_hex: &str, 631 ) -> Result<RadrootsIdentity, RadrootsAppRemoteSignerError> { 632 RadrootsIdentity::from_secret_key_str(client_secret_key_hex) 633 .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string())) 634 } 635 636 fn target_for_record( 637 record: &RadrootsAppRemoteSignerSessionRecord, 638 ) -> RadrootsAppRemoteSignerTarget { 639 RadrootsAppRemoteSignerTarget { 640 source: crate::RadrootsAppRemoteSignerSource::BunkerUri, 641 signer_identity: record.signer_identity.clone(), 642 relays: record.relays.clone(), 643 connect_secret: None, 644 requested_permissions: if record.approved_permissions.is_empty() { 645 crate::radroots_app_remote_signer_requested_permissions() 646 } else { 647 record.approved_permissions.clone() 648 }, 649 } 650 } 651 652 #[cfg(test)] 653 mod tests { 654 use super::*; 655 use crate::radroots_app_remote_signer_preview; 656 use radroots_identity::RadrootsIdentity; 657 use radroots_nostr_connect::prelude::{ 658 RadrootsNostrConnectPermission, RadrootsNostrConnectRemoteSessionCapability, 659 }; 660 661 const RELAY_PRIMARY_WSS: &str = "wss://relay.example.com"; 662 const SIGNER_SECRET_KEY_HEX: &str = 663 "1111111111111111111111111111111111111111111111111111111111111111"; 664 const CLIENT_SECRET_KEY_HEX: &str = 665 "2222222222222222222222222222222222222222222222222222222222222222"; 666 667 fn fixture_identity(secret_key_hex: &str) -> RadrootsIdentity { 668 RadrootsIdentity::from_secret_key_str(secret_key_hex).expect("identity") 669 } 670 671 fn fixture_public_key() -> nostr::PublicKey { 672 fixture_identity(SIGNER_SECRET_KEY_HEX).public_key() 673 } 674 675 fn fixture_discovery_url() -> String { 676 format!( 677 "http://localhost/connect?uri={}", 678 url::form_urlencoded::byte_serialize( 679 format!( 680 "bunker://{}?relay={RELAY_PRIMARY_WSS}", 681 fixture_identity(SIGNER_SECRET_KEY_HEX).public_key_hex() 682 ) 683 .as_bytes() 684 ) 685 .collect::<String>() 686 ) 687 } 688 689 #[test] 690 fn pending_connection_response_is_classified_as_pending_approval() { 691 let outcome = 692 classify_pending_poll_response(RadrootsNostrConnectResponse::PendingConnection); 693 694 assert!(matches!( 695 outcome, 696 RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval 697 )); 698 } 699 700 #[test] 701 fn signer_error_response_is_classified_as_rejected() { 702 let outcome = classify_pending_poll_response(RadrootsNostrConnectResponse::Error { 703 result: None, 704 error: "unauthorized".to_owned(), 705 }); 706 707 assert!(matches!( 708 outcome, 709 RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message } 710 if message == "unauthorized" 711 )); 712 } 713 714 #[test] 715 fn session_capability_success_is_classified_as_approved() { 716 let outcome = 717 classify_pending_poll_response(RadrootsNostrConnectResponse::RemoteSessionCapability( 718 RadrootsNostrConnectRemoteSessionCapability { 719 user_public_key: fixture_public_key(), 720 relays: vec![nostr::RelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay")], 721 permissions: vec![ 722 RadrootsNostrConnectPermission::with_parameter( 723 RadrootsNostrConnectMethod::SignEvent, 724 "kind:1", 725 ), 726 RadrootsNostrConnectPermission::new( 727 RadrootsNostrConnectMethod::SwitchRelays, 728 ), 729 ] 730 .into(), 731 }, 732 )); 733 734 assert!(matches!( 735 outcome, 736 RadrootsAppRemoteSignerPendingPollOutcome::Approved( 737 RadrootsAppRemoteSignerApprovedSession { user_identity, approved_permissions, .. } 738 ) if user_identity.public_key_hex == fixture_public_key().to_hex() 739 && approved_permissions.to_string() == "sign_event:kind:1,switch_relays" 740 )); 741 } 742 743 #[test] 744 fn timeout_error_is_classified_as_transport_failure() { 745 let outcome = classify_pending_poll_error(RadrootsAppRemoteSignerError::RequestTimedOut { 746 method: RadrootsNostrConnectMethod::GetSessionCapability, 747 }); 748 749 assert!(matches!( 750 outcome, 751 RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message } 752 if message == "remote signer did not respond yet" 753 )); 754 } 755 756 #[test] 757 fn unexpected_response_error_is_fatal() { 758 let outcome = 759 classify_pending_poll_error(RadrootsAppRemoteSignerError::UnexpectedResponse { 760 method: RadrootsNostrConnectMethod::GetSessionCapability, 761 response: "failed to decode signer response envelope: bad".to_owned(), 762 }); 763 764 assert!(matches!( 765 outcome, 766 RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message } 767 if message.contains("unexpected `get_session_capability` response") 768 )); 769 } 770 771 #[test] 772 fn connect_request_uses_explicit_requested_permissions() { 773 let target = 774 radroots_app_remote_signer_preview(fixture_discovery_url().as_str()).expect("preview"); 775 776 let request = connect_request_for_target(&target); 777 778 match request { 779 RadrootsNostrConnectRequest::Connect { 780 requested_permissions, 781 .. 782 } => assert_eq!( 783 requested_permissions.to_string(), 784 "sign_event:kind:1,switch_relays" 785 ), 786 other => panic!("unexpected request: {other:?}"), 787 } 788 } 789 790 #[test] 791 fn sign_kind1_note_output_carries_signed_relay_state() { 792 let signed_event = RadrootsAppRemoteSignerSignedEvent { 793 event_id_hex: "deadbeef".to_owned(), 794 event_json: "{\"id\":\"deadbeef\"}".to_owned(), 795 relays: vec!["ws://localhost:8080".to_owned()], 796 }; 797 798 assert_eq!(signed_event.event_id_hex, "deadbeef"); 799 assert_eq!(signed_event.relays, vec!["ws://localhost:8080".to_owned()]); 800 } 801 802 #[test] 803 fn target_for_record_uses_approved_permissions_when_available() { 804 let client_identity = fixture_identity(CLIENT_SECRET_KEY_HEX).to_public(); 805 let signer_identity = fixture_identity(SIGNER_SECRET_KEY_HEX).to_public(); 806 let mut record = RadrootsAppRemoteSignerSessionRecord::pending( 807 client_identity, 808 signer_identity, 809 vec![RELAY_PRIMARY_WSS.to_owned()], 810 ); 811 record.approved_permissions = vec![RadrootsNostrConnectPermission::new( 812 RadrootsNostrConnectMethod::SwitchRelays, 813 )] 814 .into(); 815 816 let target = target_for_record(&record); 817 818 assert_eq!(target.requested_permissions.to_string(), "switch_relays"); 819 } 820 }