+
+
{{ t('web5.yourTorAddress') }}
@@ -59,8 +55,48 @@
+
+
+
+
Discoverable nodes
+
+ {{ discovering ? 'Searching…' : 'Refresh' }}
+
+
+
+ Searching relays…
+
+
+ No discoverable nodes found yet. Nodes appear here as relays gossip their presence.
+
+
+
+
+
{{ shortNpub(node.nostr_npub) }}
+
{{ node.did }}
+
version {{ node.version || '?' }}
+
+
+ {{ requestedPeers.has(node.nostr_pubkey) ? 'Requested' : requestingPeer === node.nostr_pubkey ? 'Sending…' : 'Request to Peer' }}
+
+
+
+
+
-
+
{{ t('web5.discoverableWarning') }}
@@ -70,6 +106,7 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
+import ToggleSwitch from '@/components/ToggleSwitch.vue'
import { safeClipboardWrite } from './utils'
import type { VisibilityLevel } from './types'
@@ -87,37 +124,65 @@ const nodeVisibility = ref
('hidden')
const nodeOnionAddress = ref(null)
const visibilityLoading = ref(false)
const settingVisibility = ref(false)
+const discoverEnabled = ref(false)
-const visibilityOptions = [
- { value: 'hidden' as VisibilityLevel, label: 'Hidden', description: 'Your node is not discoverable by others' },
- { value: 'discoverable' as VisibilityLevel, label: 'Discoverable', description: 'Federated peers can find and connect to your node' },
- { value: 'public' as VisibilityLevel, label: 'Public', description: 'Accepting connections from any Archipelago node' },
-]
+interface DiscoverableNode {
+ nostr_pubkey: string
+ nostr_npub: string
+ did: string
+ version: string
+}
+
+const discoveredNodes = ref([])
+const discovering = ref(false)
+const requestingPeer = ref(null)
+const requestedPeers = ref(new Set())
+
+function shortNpub(npub: string): string {
+ if (!npub) return 'unknown'
+ return npub.length > 21 ? `${npub.slice(0, 12)}…${npub.slice(-6)}` : npub
+}
async function loadVisibility() {
visibilityLoading.value = true
try {
- const res = await rpcClient.call<{ visibility: string; onion_address?: string }>({ method: 'network.get-visibility' })
- nodeVisibility.value = (res.visibility as VisibilityLevel) || 'hidden'
- nodeOnionAddress.value = res.onion_address || null
+ // Nostr discovery is the functional flag: when on, a presence event
+ // (DID + npub — never the onion) is published to public relays so
+ // ANYONE can find this node and request a connection. The legacy
+ // network.get-visibility tri-state is only read for the onion display.
+ const [disc, vis] = await Promise.all([
+ rpcClient.nostrDiscoveryStatus(),
+ rpcClient
+ .call<{ visibility: string; onion_address?: string; tor_address?: string }>({ method: 'network.get-visibility' })
+ .catch(() => null),
+ ])
+ discoverEnabled.value = !!disc.enabled
+ nodeVisibility.value = (vis?.visibility as VisibilityLevel) || 'hidden'
+ nodeOnionAddress.value = vis?.onion_address || vis?.tor_address || null
+ if (discoverEnabled.value) void discoverNodes()
} catch {
- nodeVisibility.value = 'hidden'
+ discoverEnabled.value = false
} finally {
visibilityLoading.value = false
}
}
-async function setVisibility(level: VisibilityLevel) {
- if (settingVisibility.value || nodeVisibility.value === level) return
+async function toggleDiscoverable(enabled: boolean) {
+ if (settingVisibility.value) return
settingVisibility.value = true
try {
- const res = await rpcClient.call<{ visibility: string; onion_address?: string }>({
- method: 'network.set-visibility',
- params: { visibility: level },
- })
- nodeVisibility.value = (res.visibility as VisibilityLevel) || level
- nodeOnionAddress.value = res.onion_address || nodeOnionAddress.value
- emit('toast', t('web5.visibilitySetTo', { level }))
+ // Public means public: the switch drives nostr presence publishing.
+ const res = await rpcClient.nostrSetDiscovery(enabled)
+ discoverEnabled.value = !!res.enabled
+ // Keep the legacy visibility string in sync (cosmetic; best-effort).
+ const level: VisibilityLevel = enabled ? 'public' : 'hidden'
+ rpcClient
+ .call({ method: 'network.set-visibility', params: { visibility: level } })
+ .then(() => { nodeVisibility.value = level })
+ .catch(() => {})
+ emit('toast', enabled ? 'Node is now publicly discoverable' : 'Node hidden from discovery')
+ if (enabled) void discoverNodes()
+ else discoveredNodes.value = []
} catch {
emit('toast', t('web5.failedToUpdateVisibility'))
} finally {
@@ -125,6 +190,36 @@ async function setVisibility(level: VisibilityLevel) {
}
}
+async function discoverNodes() {
+ if (discovering.value) return
+ discovering.value = true
+ try {
+ const res = await rpcClient.handshakeDiscover()
+ discoveredNodes.value = res.nodes || []
+ } catch {
+ // keep whatever we had; the empty-state copy explains relay gossip lag
+ } finally {
+ discovering.value = false
+ }
+}
+
+async function requestToPeer(node: DiscoverableNode) {
+ if (requestingPeer.value) return
+ requestingPeer.value = node.nostr_pubkey
+ try {
+ // Connection requests always land as Peer (observer) on approval —
+ // never trusted — so a mistaken request can't hand over fleet access.
+ await rpcClient.handshakeConnect(node.nostr_pubkey)
+ requestedPeers.value.add(node.nostr_pubkey)
+ requestedPeers.value = new Set(requestedPeers.value)
+ emit('toast', 'Peer request sent — awaiting their approval')
+ } catch (e) {
+ emit('toast', e instanceof Error ? e.message : 'Failed to send peer request')
+ } finally {
+ requestingPeer.value = null
+ }
+}
+
function copyOnionAddress() {
if (!nodeOnionAddress.value) return
safeClipboardWrite(nodeOnionAddress.value)