From b288be314ce4092197f03eac07371c770b858d28 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 22 Jul 2026 02:55:06 -0400 Subject: [PATCH] feat(federation): presence-signing overlay on discovery enable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling Nostr discoverability now opens a signer overlay (same visual language as the app NIP-07 identity picker) before anything is published: a locked signer row fixed to the node's discovery key — deliberately not pickable, personal identities are never used — plus the exact signed content (DID, npub, software version, kind 30078/NIP-33 format) and a Sign & Publish confirm. Disabling stays immediate. Also seeds the mock with discovery OFF to match the production default, and defaults dwnSyncLabel to 'Unknown' when the backend omits sync_status. Co-Authored-By: Claude Fable 5 --- neode-ui/mock-backend.js | 4 +- neode-ui/src/views/Federation.vue | 41 ++- .../views/federation/PresenceSignModal.vue | 244 ++++++++++++++++++ 3 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 neode-ui/src/views/federation/PresenceSignModal.vue diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 072aa1ab..0a3779b0 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -228,7 +228,9 @@ function seedMockState() { return { analyticsEnabled: false, nodeVisibility: 'discoverable', - nostrDiscovery: true, + // Discovery starts OFF, matching the production default — enabling it in + // the UI walks through the presence-signing overlay. + nostrDiscovery: false, pendingPeerRequests: [ { id: 'preq-demo-1', diff --git a/neode-ui/src/views/Federation.vue b/neode-ui/src/views/Federation.vue index 81592b9d..08e43939 100644 --- a/neode-ui/src/views/Federation.vue +++ b/neode-ui/src/views/Federation.vue @@ -212,6 +212,17 @@ @close="showDiscoverModal = false" @sent="loadPendingRequests" /> + + @@ -230,6 +241,7 @@ import NodeDetailModal from './federation/NodeDetailModal.vue' import JoinModal from './federation/JoinModal.vue' import PendingRequestsPanel from './federation/PendingRequestsPanel.vue' import DiscoverModal from './federation/DiscoverModal.vue' +import PresenceSignModal from './federation/PresenceSignModal.vue' import type { FederatedNode, DwnStatus, SyncResult } from './federation/types' import type { PendingPeerRequest } from '@/api/rpc-client' import { nodeName, timeAgo } from './federation/utils' @@ -327,7 +339,7 @@ const dwnSyncLabel = computed(() => { case 'synced': return 'Synced' case 'syncing': return 'Syncing...' case 'error': return 'Error' - default: return dwnStatus.value.sync_status + default: return dwnStatus.value.sync_status || 'Unknown' } }) @@ -348,6 +360,7 @@ 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 showPresenceSignModal = ref(false) const nodeNpub = ref('') const appVersion = computed(() => syncStore.serverInfo?.version || '') @@ -378,11 +391,19 @@ async function loadDiscoveryState() { } async function toggleDiscovery() { + if (!discoveryEnabled.value) { + // Enabling publishes a signed presence event — surface the signer overlay + // first so the user sees exactly what gets signed and with which key. + // Nothing is published until they confirm. + if (!nodeNpub.value) loadNodeNpub() + showPresenceSignModal.value = true + return + } + // Disabling publishes nothing new — apply immediately. discoveryToggling.value = true discoveryError.value = '' - const next = !discoveryEnabled.value try { - const result = await rpcClient.nostrSetDiscovery(next) + const result = await rpcClient.nostrSetDiscovery(false) discoveryEnabled.value = !!result.enabled } catch (e: unknown) { discoveryError.value = e instanceof Error ? e.message : 'Failed to toggle discoverability' @@ -391,6 +412,20 @@ async function toggleDiscovery() { } } +async function confirmPresenceSign() { + discoveryToggling.value = true + discoveryError.value = '' + try { + const result = await rpcClient.nostrSetDiscovery(true) + discoveryEnabled.value = !!result.enabled + showPresenceSignModal.value = false + } catch (e: unknown) { + discoveryError.value = e instanceof Error ? e.message : 'Failed to enable discoverability' + } finally { + discoveryToggling.value = false + } +} + async function loadPendingRequests() { try { const result = await rpcClient.federationListPendingRequests() diff --git a/neode-ui/src/views/federation/PresenceSignModal.vue b/neode-ui/src/views/federation/PresenceSignModal.vue new file mode 100644 index 00000000..ad796b70 --- /dev/null +++ b/neode-ui/src/views/federation/PresenceSignModal.vue @@ -0,0 +1,244 @@ + + + + +