goblin: identity backend fixes for Build 143
- No "Primary" (or any placeholder) label: label_for now yields the claimed name's local part or EMPTY, never a placeholder word. The UI derives its display from the name or a truncated npub, so an unnamed identity is shown by its npub, never "Primary". - Add Wallet::verify_nostr_password so the identity password modal can reject a wrong password up front (in-memory NIP-49 unlock of the active identity), matching the wallet-open modal, before spawning an add/switch worker. - Regression test: importing a bare nsec adds it as a held identity via the same NIP-49 encrypted store, keyed by its own pubkey, openable under the wallet password, with the active pointer unchanged (add never switches).
This commit is contained in:
+37
-7
@@ -63,9 +63,11 @@ pub struct HeldEntry {
|
||||
/// `"identity.json"` for the legacy identity #1, else
|
||||
/// `"identities/<hex>/identity.json"`.
|
||||
pub path: String,
|
||||
/// A short human label (its @name, or "Primary" / a fallback). Plaintext by
|
||||
/// design (this index carries no secret); the npub/name are recoverable from
|
||||
/// the referenced file anyway.
|
||||
/// A short human label: the identity's claimed name (local part of its NIP-05)
|
||||
/// when it has one, else empty. NOT rendered — the UI derives its display from
|
||||
/// the name or a truncated npub — kept only as a convenience field in the
|
||||
/// index. Plaintext by design (this index carries no secret). Never a
|
||||
/// placeholder word.
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
@@ -322,15 +324,17 @@ pub enum HeldError {
|
||||
Io(String),
|
||||
}
|
||||
|
||||
/// A short display label for an identity: its @name when claimed, else "Primary".
|
||||
/// Never includes secret material.
|
||||
/// A convenience label for an identity: the local part of its claimed NIP-05
|
||||
/// name when it has one, else EMPTY (never a placeholder word — an unnamed
|
||||
/// identity is shown by its truncated npub in the UI, not by a label). Never
|
||||
/// includes secret material.
|
||||
fn label_for(id: &NostrIdentity) -> String {
|
||||
id.nip05
|
||||
.as_deref()
|
||||
.and_then(|n| n.split('@').next())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "Primary".to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The catch-up `since` (unix seconds) for the identity we are bringing up. We
|
||||
@@ -493,7 +497,7 @@ mod tests {
|
||||
identities: vec![HeldEntry {
|
||||
pubkey: legacy_hex.clone(),
|
||||
path: "identity.json".to_string(),
|
||||
label: "Primary".to_string(),
|
||||
label: String::new(),
|
||||
}],
|
||||
};
|
||||
idx.save(&dir).unwrap();
|
||||
@@ -522,6 +526,32 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imported_nsec_adds_as_held_identity_and_unlocks() {
|
||||
// The multi-identity import path: a bare nsec becomes a held identity via
|
||||
// the same NIP-49 encrypted store, keyed by its own pubkey, openable under
|
||||
// the wallet password. (Regression guard for the add-import flow.)
|
||||
use nostr_sdk::{Keys, ToBech32};
|
||||
let dir = tmpdir("import");
|
||||
let (legacy, _) = NostrIdentity::create_random("pw").unwrap();
|
||||
legacy.save(&dir).unwrap();
|
||||
let (mut idx, _) = HeldIdentities::load_or_migrate(&dir, &legacy).unwrap();
|
||||
|
||||
// A distinct external key, as an nsec string.
|
||||
let ext = Keys::generate();
|
||||
let nsec = ext.secret_key().to_bech32().unwrap();
|
||||
let (imported, _) = NostrIdentity::create_imported(&nsec, "pw").unwrap();
|
||||
let hex = idx.add(&dir, &imported).unwrap();
|
||||
|
||||
// Held, active pointer unchanged (add never switches), file openable.
|
||||
assert_eq!(idx.len(), 2);
|
||||
assert_eq!(idx.active, legacy.pubkey_hex().unwrap());
|
||||
let stored = idx.entry(&hex).unwrap().load(&dir).unwrap();
|
||||
assert_eq!(stored.source, crate::nostr::IdentitySource::Imported);
|
||||
assert_eq!(stored.unlock("pw").unwrap().public_key(), ext.public_key());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catchup_since_prefers_identity_then_wallet_then_now() {
|
||||
let lookback = 3 * 86_400;
|
||||
|
||||
@@ -760,6 +760,16 @@ impl Wallet {
|
||||
self.nostr_service().map(|s| s.public_key().to_hex())
|
||||
}
|
||||
|
||||
/// Verify a candidate wallet password against the active nostr identity
|
||||
/// (cheap, in-memory NIP-49 unlock). Lets the identity password modal reject a
|
||||
/// wrong password up front — before spawning an add/switch worker — the way the
|
||||
/// wallet-open modal does. `false` when nostr is not running.
|
||||
pub fn verify_nostr_password(&self, password: &str) -> bool {
|
||||
self.nostr_service()
|
||||
.map(|s| s.identity.read().unlock(password).is_ok())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Add a nostr identity to this wallet WITHOUT switching to it: generate a
|
||||
/// fresh random nsec (`import` is `None`) or import an existing one (`import`
|
||||
/// is `Some(nsec)`). The new key is encrypted under the wallet password like
|
||||
|
||||
Reference in New Issue
Block a user