use super::account::{device_approval_bootstrap_label, device_approval_pairing_invite};
use super::*;
impl AppCore {
pub(super) fn start_linked_device(&mut self, _owner_input: &str) {
self.push_debug_log("session.start_linked", "create device approval request");
self.state.busy.linking_device = true;
self.emit_state();
if let Err(error) = self.create_pending_linked_device() {
self.state.toast = Some(error.to_string());
}
self.state.busy.linking_device = false;
self.screen_stack = vec![Screen::AddDevice];
self.rebuild_state();
self.emit_state();
}
fn create_pending_linked_device(&mut self) -> anyhow::Result<()> {
self.stop_pending_linked_device();
if self.device_approval_relay_urls.len() != 1 {
anyhow::bail!("Device approval requires exactly one approval relay.");
}
let device_keys = Keys::generate();
let device_pubkey = device_keys.public_key();
let current_device_labels = self.current_device_labels.clone();
let local_request = create_nostr_identity_device_approval_request(
&device_keys,
CreateNostrIdentityDeviceApprovalRequestOptions {
request_keys: None,
request_secret: None,
requested_at: i64::try_from(unix_now().get()).unwrap_or(i64::MAX),
request_type: Some("device_link".to_string()),
resources: Vec::new(),
expires_at: None,
profile_id: None,
admin_app_key_pubkey: None,
label: current_device_labels
.as_ref()
.and_then(|labels| labels.device_label.as_deref())
.and_then(device_approval_bootstrap_label),
},
)?;
let invite =
device_approval_pairing_invite(device_pubkey, &local_request.request.request_secret)?;
let bootstrap = nostr_identity_device_approval_bootstrap(&local_request.request)?;
let url = encode_nostr_identity_device_approval_bootstrap(&bootstrap, None)?;
self.install_pending_linked_device(device_keys, bootstrap, invite, url, true);
Ok(())
}
pub(super) fn restore_pending_linked_device(
&mut self,
device_nsec: &str,
approval_bootstrap_json: &str,
) {
self.deferred_startup_restore = None;
self.state.busy.restoring_session = true;
self.emit_state();
let result = (|| -> anyhow::Result<()> {
let device_keys = Keys::parse(device_nsec.trim())?;
let bootstrap: NostrIdentityDeviceApprovalBootstrap =
serde_json::from_str(approval_bootstrap_json)?;
if PublicKey::parse(&bootstrap.device_app_key_npub)? != device_keys.public_key() {
anyhow::bail!("Stored device link does not match its secret key.");
}
let invite = device_approval_pairing_invite(
device_keys.public_key(),
&bootstrap.request_secret,
)?;
let url = encode_nostr_identity_device_approval_bootstrap(&bootstrap, None)?;
self.stop_pending_linked_device();
self.install_pending_linked_device(device_keys, bootstrap, invite, url, false);
Ok(())
})();
if let Err(error) = result {
self.state.toast = Some(error.to_string());
let _ = self.update_tx.send(AppUpdate::ClearPendingDeviceLink);
}
self.state.busy.restoring_session = false;
self.screen_stack = vec![Screen::AddDevice];
self.rebuild_state();
self.emit_state();
}
}
pub(super) fn validate_link_authorization(
event: &Event,
owner: PublicKey,
device: &Keys,
peer_device_id: &str,
) -> anyhow::Result<()> {
if !link_approver_is_authorized(event, owner, peer_device_id)
|| resolve_app_keys_owner_for_device(event, device.public_key())? != Some(owner)
{
anyhow::bail!("Link authorization is incomplete.");
}
Ok(())
}
pub(super) fn link_approver_is_authorized(
event: &Event,
owner: PublicKey,
peer_device_id: &str,
) -> bool {
if event.pubkey != owner {
return false;
}
let Ok(peer_device) = PublicKey::from_hex(peer_device_id) else {
return false;
};
let Ok(app_keys) = AppKeys::from_event(event) else {
return false;
};
peer_device == owner || app_keys.get_device(&peer_device).is_some()
}
pub(super) async fn connect_pending_device_link_client(client: &Client, relay_urls: &[RelayUrl]) {
for (url, relay) in client.relays().await {
if relay_urls.contains(&url) && relay.status() == RelayStatus::Terminated {
let _ = client.force_remove_relay(url).await;
}
}
ensure_session_relays_configured(client, relay_urls).await;
connect_client_with_timeout(
client,
Duration::from_secs(PENDING_DEVICE_LINK_CONNECT_TIMEOUT_SECS),
)
.await;
}