From 50d5d4d1328b018e292b594d08eec8d4ea7ebe2b Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 22 Jul 2026 02:18:58 -0400 Subject: [PATCH] feat(federation): signing-details panel on the Nostr discoverability strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'Signing details' disclosure showing who signs the presence event — a locked identity row (styled like the NostrIdentityPicker overlay) fixed to the node's own discovery key, deliberately not pickable so personal identities can never be used for node discovery — plus a human-readable breakdown of the signed content: DID, signing npub, software version, and the event format (kind 30078, NIP-33 replaceable, d-tag archipelago-node), with a note that every event carries a NIP-01 Schnorr signature. Co-Authored-By: Claude Fable 5 --- neode-ui/src/views/Federation.vue | 150 ++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 28 deletions(-) diff --git a/neode-ui/src/views/Federation.vue b/neode-ui/src/views/Federation.vue index f96a215f..81592b9d 100644 --- a/neode-ui/src/views/Federation.vue +++ b/neode-ui/src/views/Federation.vue @@ -52,36 +52,108 @@ manages everything related to peering. The toggle directly mutates the `nostr_discovery_enabled` config flag — backend defaults to OFF and nothing is published until the user explicitly turns it on. --> -
-
-
- Nostr discoverability - {{ discoveryEnabled ? 'On' : 'Off' }} +
+
+
+
+ Nostr discoverability + {{ discoveryEnabled ? 'On' : 'Off' }} +
+

+ When on, this node publishes a presence event (DID + npub only — never an onion) + so other nodes can find you and request to peer. Inbound requests land in the + panel below for your approval. Off by default. +

+ +
+
+ +
-

- When on, this node publishes a presence event (DID + npub only — never an onion) - so other nodes can find you and request to peer. Inbound requests land in the - panel below for your approval. Off by default. -

-
- - + + +
+
+ + +
+
+
+ {{ (appStore.serverName || 'N').charAt(0).toUpperCase() }} +
+
+
+ {{ appStore.serverName || 'This node' }} + node identity +
+
+ {{ nodeNpub ? truncateNpub(nodeNpub) : 'loading…' }} +
+
+ + + +
+
+

+ Presence events are always signed with this node's dedicated discovery key. + Your personal identities are never used for node discovery, so unlike the app + sign-in overlay there is nothing to choose here. +

+
+
+ +
+
+
Identity (DID)
+
{{ selfDid || '—' }}
+
+
+
Signing key
+
{{ nodeNpub || '—' }}
+
+
+
Software version
+
{{ appVersion || '—' }}
+
+
+
Event format
+
Nostr kind 30078, replaceable (NIP-33), tag archipelago-node
+
+
+

+ Every presence event carries a Schnorr signature (NIP-01) made with the key + above — relays reject unsigned events, so anything you see via discovery was + cryptographically signed by its node. Your onion address is never part of this + event; it is only shared over encrypted DMs (NIP-44) after you approve a peer. +

+
{{ discoveryError }}
@@ -148,6 +220,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { rpcClient } from '@/api/rpc-client' import { useTransportStore } from '@/stores/transport' import { useAppStore } from '@/stores/app' +import { useSyncStore } from '@/stores/sync' import NetworkMap from '@/components/federation/NetworkMap.vue' import FederationHeader from './federation/FederationHeader.vue' import RotateDidModal from './federation/RotateDidModal.vue' @@ -163,6 +236,7 @@ import { nodeName, timeAgo } from './federation/utils' const transportStore = useTransportStore() const appStore = useAppStore() +const syncStore = useSyncStore() const nodes = ref([]) const loading = ref(true) @@ -271,6 +345,25 @@ const discoveryEnabled = ref(false) const discoveryToggling = ref(false) const discoveryError = ref('') const showDiscoverModal = ref(false) +// Signing-details disclosure: who signs the presence event (always the node's +// own discovery key) and a human-readable view of the signed content. +const showSigningInfo = ref(false) +const nodeNpub = ref('') +const appVersion = computed(() => syncStore.serverInfo?.version || '') + +function truncateNpub(npub: string): string { + if (npub.length <= 20) return npub + return npub.slice(0, 12) + '...' + npub.slice(-6) +} + +async function loadNodeNpub() { + try { + const res = await rpcClient.call<{ nostr_npub?: string }>({ method: 'node.nostr-pubkey' }) + if (res?.nostr_npub) nodeNpub.value = res.nostr_npub + } catch { + // Nostr key not provisioned yet — panel shows a placeholder + } +} const pendingRequests = ref([]) const pollingHandshake = ref(false) const pendingBusyId = ref(null) @@ -557,6 +650,7 @@ onMounted(async () => { loadDwnStatus() loadDiscoveryState() loadPendingRequests() + loadNodeNpub() transportStore.fetchPeers() try { const result = await rpcClient.getNodeDid()