fix(profiles): label-only rename for secretless remote-signer profiles

This commit is contained in:
Avi 2026-09-23 14:44:18 -05:00
commit 6ebc364fcf
2 changed files with 66 additions and 3 deletions

View file

@ -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,

View file

@ -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();