fix(identity): Node npub in Web5 Identities matches Settings (#49)

Settings shows the node-level Nostr key (HKDF derive_node_nostr_key,
read via node.nostr-pubkey) while Web5 > Identities showed the identity
record's own key — the mirrored "Node" identity stores nostr=None and
seed identities use a different BIP-32 NIP-06 key, so the two surfaces
disagreed.

Resolve the node-level Nostr key once in identity.list and override it
onto whichever identity record is the node's own (ed25519 == server_info
.pubkey). Display-only — no stored key is rewritten, so it self-applies
to existing nodes with no migration and the discovery identity is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-17 06:03:25 -04:00
parent 6de8173d18
commit 56752ebfc0

View File

@ -14,10 +14,39 @@ impl RpcHandler {
let manager = IdentityManager::new(&self.config.data_dir).await?; let manager = IdentityManager::new(&self.config.data_dir).await?;
let (identities, default_id) = manager.list().await?; let (identities, default_id) = manager.list().await?;
// #49: The canonical node Nostr key is the node-level HKDF key
// (`derive_node_nostr_key`) that Settings and Nostr discovery both use
// via `node.nostr-pubkey`. The mirrored "Node" identity stores
// nostr=None, and seed identities use a different BIP-32 NIP-06 key, so
// the "Node" entry in Web5 > Identities disagreed with Settings. Resolve
// the node-level key once and override it onto whichever identity record
// is the node's own (its ed25519 matches `server_info.pubkey`), so both
// surfaces always show the same npub. Display-only — no key is rewritten.
let identity_dir = self.config.data_dir.join("identity");
let node_nostr_hex = crate::nostr_discovery::get_nostr_pubkey(&identity_dir)
.await
.ok();
let node_nostr_npub = node_nostr_hex.as_ref().and_then(|h| {
nostr_sdk::PublicKey::from_hex(h)
.ok()
.and_then(|pk| pk.to_bech32().ok())
});
let (snapshot, _) = self.state_manager.get_snapshot().await;
let node_pubkey_hex = snapshot.server_info.pubkey.clone();
let items: Vec<serde_json::Value> = identities let items: Vec<serde_json::Value> = identities
.into_iter() .into_iter()
.map(|id| { .map(|id| {
let is_default = default_id.as_deref() == Some(&id.id); let is_default = default_id.as_deref() == Some(&id.id);
let is_node = !node_pubkey_hex.is_empty() && id.pubkey_hex == node_pubkey_hex;
let (nostr_pubkey, nostr_npub) = if is_node {
(
node_nostr_hex.clone().or(id.nostr_pubkey),
node_nostr_npub.clone().or(id.nostr_npub),
)
} else {
(id.nostr_pubkey, id.nostr_npub)
};
serde_json::json!({ serde_json::json!({
"id": id.id, "id": id.id,
"name": id.name, "name": id.name,
@ -26,8 +55,8 @@ impl RpcHandler {
"did": id.did, "did": id.did,
"created_at": id.created_at, "created_at": id.created_at,
"is_default": is_default, "is_default": is_default,
"nostr_pubkey": id.nostr_pubkey, "nostr_pubkey": nostr_pubkey,
"nostr_npub": id.nostr_npub, "nostr_npub": nostr_npub,
"profile": id.profile, "profile": id.profile,
}) })
}) })