From 6ebc364fcf62c6d67a939dadc4d911e2f52678d8 Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 23 Sep 2026 14:44:18 -0500 Subject: [PATCH] fix(profiles): label-only rename for secretless remote-signer profiles --- frontend/src/screens/ProfilesScreen.tsx | 8 ++-- src/profiles.rs | 61 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/frontend/src/screens/ProfilesScreen.tsx b/frontend/src/screens/ProfilesScreen.tsx index 60c292b..200700a 100644 --- a/frontend/src/screens/ProfilesScreen.tsx +++ b/frontend/src/screens/ProfilesScreen.tsx @@ -576,9 +576,11 @@ function RenameModal({ npub: target.npub, perform: () => renameProfile(target.npub, trimmed), successMessage: (report) => - report.failed.length === 0 - ? `Renamed to "${trimmed}" and published to ${report.succeeded.length} relay(s). It may take a minute to appear on other clients.` - : `Renamed to "${trimmed}", but ${report.failed.length} relay(s) did not accept it. Use "Publish name" to retry.`, + report.succeeded.length === 0 && report.failed.length === 0 + ? `Renamed to "${trimmed}" (saved on this device; external-signer profiles publish their name from the signer app).` + : report.failed.length === 0 + ? `Renamed to "${trimmed}" and published to ${report.succeeded.length} relay(s). It may take a minute to appear on other clients.` + : `Renamed to "${trimmed}", but ${report.failed.length} relay(s) did not accept it. Use "Publish name" to retry.`, onSaved, onError, onSavingChange, diff --git a/src/profiles.rs b/src/profiles.rs index 65a4dfa..0d6e146 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -298,6 +298,39 @@ pub fn rename_profile( return Err(AppError::config("The profile name cannot be empty.")); } + // Remote (secretless) profiles have no local key to sign a kind-0 + // metadata event with — and the kind-0 reroute through the external + // signer is still pending (P2). The label is still useful as the local + // display name, so rename it vault-side and report zero relays rather + // than failing the whole rename outright. + let is_remote = vault + .profiles + .iter() + .find(|p| p.public_key == npub) + .is_some_and(|p| { + p.signer_mode == SignerMode::Nip46Client || p.secret_key.trim().is_empty() + }); + if is_remote { + let is_active = vault.active_profile.as_deref() == Some(npub); + let stored = find_profile_mut(vault, npub)?; + stored.label = trimmed.to_string(); + let summary = ProfileSummary { + label: stored.label.clone(), + npub: stored.public_key.clone(), + created_at: stored.created_at, + is_active, + picture: stored.picture.clone(), + nip05: stored.nip05.clone(), + }; + return Ok(( + summary, + MetadataPublishReport { + succeeded: Vec::new(), + failed: Vec::new(), + }, + )); + } + // Resolve and sign before mutating so a locked vault or bad key changes // nothing on disk. let secret_hex = resolve_secret_key(vault, npub, key)?; @@ -1230,6 +1263,34 @@ mod tests { assert_eq!(err.kind(), crate::errors::ErrorKind::ProfileNotFound); } + #[test] + fn rename_profile_updates_label_for_secretless_remote_profiles() { + // Paired (NIP-46) profiles carry no local key, so no kind-0 can be + // signed — but the local display label must still be renameable. + use nostr::nips::nip19::ToBech32; + let mut vault = Vault::empty(); + let remote = Keys::generate(); + let npub = remote.public_key().to_bech32().unwrap(); + store_remote_profile(&mut vault, &npub, "Remote Signer".to_string()).unwrap(); + + let (renamed, report) = rename_profile( + &mut vault, + &npub, + "Phone Key".to_string(), + None, + &offline_settings(), + ) + .expect("remote rename must succeed locally"); + assert_eq!(renamed.label, "Phone Key"); + assert_eq!(vault.profiles[0].label, "Phone Key"); + assert!( + vault.profiles[0].secret_key.is_empty(), + "rename must not fabricate a secret" + ); + assert!(report.succeeded.is_empty()); + assert!(report.failed.is_empty()); + } + #[test] fn set_nip05_stores_identifier_and_skips_publish_without_relays() { let mut vault = Vault::empty();