feat(settings): show DID on every node + add seed-derived node npub (#13)

- DID: the Identity card read the DID only from localStorage('neode_did'), so
  nodes/browsers that never cached it (e.g. .116/.228) showed no DID. Fall back
  to the node.did RPC and cache it — the DID now shows everywhere.
- npub: add the node's seed-derived Nostr public key (npub) to the Identity card
  next to the DID + onion, fetched from node.nostr-pubkey, with a copy button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-16 09:37:09 -04:00
parent aa9e0f02b7
commit 9a518db7b8

View File

@ -50,13 +50,19 @@ useBodyScrollLock(showReleaseNotes)
const serverTorAddressFromStore = computed(() => store.serverInfo?.['tor-address'] || null)
const torAddressFromRpc = ref<string | null>(null)
const serverTorAddress = computed(() => serverTorAddressFromStore.value || torAddressFromRpc.value)
// Fallback DID fetched from the backend when localStorage doesn't have one
// (e.g. a browser/node where onboarding never stored `neode_did`).
const didFromRpc = ref<string | null>(null)
const userDid = computed(() => {
try {
return localStorage.getItem('neode_did') || null
return localStorage.getItem('neode_did') || didFromRpc.value
} catch {
return null
return didFromRpc.value
}
})
// The node's seed-derived Nostr public key (npub), fetched from the backend.
const userNpub = ref<string | null>(null)
const copiedNpub = ref(false)
const copiedOnion = ref(false)
const copiedDid = ref(false)
@ -100,6 +106,17 @@ async function copyDid() {
setTimeout(() => { copiedDid.value = false }, 2000)
}
async function copyNpub() {
if (!userNpub.value) return
try {
await navigator.clipboard.writeText(userNpub.value)
} catch {
return
}
copiedNpub.value = true
setTimeout(() => { copiedNpub.value = false }, 2000)
}
// Load Tor address on mount if not in store
async function init() {
if (!serverTorAddressFromStore.value) {
@ -110,6 +127,29 @@ async function init() {
if (import.meta.env.DEV) console.warn('Tor address may not be available yet', e)
}
}
// DID: fall back to the node.did RPC when localStorage doesn't have one, so
// the Identity card shows the DID on every node (not just ones where the
// browser cached it during onboarding).
let storedDid: string | null = null
try { storedDid = localStorage.getItem('neode_did') } catch { /* unavailable */ }
if (!storedDid) {
try {
const res = await rpcClient.call<{ did?: string }>({ method: 'node.did' })
if (res?.did) {
didFromRpc.value = res.did
try { localStorage.setItem('neode_did', res.did) } catch { /* unavailable */ }
}
} catch (e) {
if (import.meta.env.DEV) console.warn('node.did unavailable', e)
}
}
// The node's seed-derived Nostr public key (npub) for the Identity card.
try {
const res = await rpcClient.call<{ nostr_npub?: string }>({ method: 'node.nostr-pubkey' })
if (res?.nostr_npub) userNpub.value = res.nostr_npub
} catch (e) {
if (import.meta.env.DEV) console.warn('node.nostr-pubkey unavailable', e)
}
}
init()
</script>
@ -1516,8 +1556,8 @@ init()
<p class="text-base font-medium text-white/90">{{ t('settings.loggedIn') }}</p>
</div>
<!-- Identity Card: DID + Tor Address -->
<div v-if="userDid || serverTorAddress" class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2 space-y-4">
<!-- Identity Card: DID + npub + Tor Address -->
<div v-if="userDid || userNpub || serverTorAddress" class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2 space-y-4">
<div v-if="userDid">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-3 min-w-0">
@ -1540,7 +1580,29 @@ init()
<p class="text-sm font-mono text-white/90 break-all" :title="userDid">{{ userDid }}</p>
<p class="text-xs text-white/50 mt-1">{{ t('settings.didHelper') }}</p>
</div>
<div v-if="serverTorAddress" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
<div v-if="userNpub" :class="userDid ? 'pt-4 border-t border-white/10' : ''">
<div class="flex items-center justify-between gap-2 mb-2">
<div class="flex items-center gap-3 min-w-0">
<svg class="w-5 h-5 text-purple-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">Node npub</p>
</div>
<button
@click="copyNpub"
class="shrink-0 px-3 py-1.5 rounded-lg glass-button glass-button-sm text-xs font-medium text-white/90 hover:text-white transition-colors flex items-center gap-1.5"
>
<svg v-if="!copiedNpub" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span v-else class="text-green-400 text-xs">{{ t('common.copied') }}</span>
<span v-if="!copiedNpub">{{ t('common.copy') }}</span>
</button>
</div>
<p class="text-sm font-mono text-white/90 break-all" :title="userNpub">{{ userNpub }}</p>
<p class="text-xs text-white/50 mt-1">Your node's Nostr public key, derived from its seed.</p>
</div>
<div v-if="serverTorAddress" :class="(userDid || userNpub) ? 'pt-4 border-t border-white/10' : ''">
<div class="flex items-center gap-3 mb-2">
<svg class="w-5 h-5 text-amber-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />