@relay-federation/bridge 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +18 -7
- package/dashboard/index.html +326 -49
- package/lib/gossip.js +300 -266
- package/lib/peer-manager.js +278 -272
- package/lib/status-server.js +2 -0
- package/package.json +1 -1
package/lib/gossip.js
CHANGED
|
@@ -1,266 +1,300 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events'
|
|
2
|
-
import { signHash, verifyHash } from '@relay-federation/common/crypto'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* GossipManager — peer discovery via WebSocket gossip protocol.
|
|
6
|
-
*
|
|
7
|
-
* Replaces HTTP-based registry scanning with pure P2P peer discovery.
|
|
8
|
-
* Three message types:
|
|
9
|
-
*
|
|
10
|
-
* getpeers — "tell me who you know"
|
|
11
|
-
* peers — response with list of known peers
|
|
12
|
-
* announce — "I'm alive" (signed, propagated to all peers)
|
|
13
|
-
*
|
|
14
|
-
* Announcements are signed with the bridge's private key to prevent
|
|
15
|
-
* impersonation. Each announcement includes a timestamp — stale
|
|
16
|
-
* announcements (older than maxAge) are discarded.
|
|
17
|
-
*
|
|
18
|
-
* Events:
|
|
19
|
-
* 'peer:discovered' — { pubkeyHex, endpoint, meshId }
|
|
20
|
-
* 'peers:response' — { peers: Array }
|
|
21
|
-
*/
|
|
22
|
-
export class GossipManager extends EventEmitter {
|
|
23
|
-
/**
|
|
24
|
-
* @param {import('./peer-manager.js').PeerManager} peerManager
|
|
25
|
-
* @param {object} opts
|
|
26
|
-
* @param {import('@bsv/sdk').PrivateKey} opts.privKey — Bridge private key for signing
|
|
27
|
-
* @param {string} opts.pubkeyHex — Our compressed pubkey hex
|
|
28
|
-
* @param {string} opts.endpoint — Our advertised WSS endpoint
|
|
29
|
-
* @param {string} [opts.meshId='70016'] — Mesh identifier
|
|
30
|
-
* @param {number} [opts.announceIntervalMs=60000] — Re-announce interval
|
|
31
|
-
* @param {number} [opts.maxAge=300000] — Max age for announcements (5 min)
|
|
32
|
-
* @param {number} [opts.maxPeersResponse=50] — Max peers in a response
|
|
33
|
-
*/
|
|
34
|
-
constructor (peerManager, opts) {
|
|
35
|
-
super()
|
|
36
|
-
this.peerManager = peerManager
|
|
37
|
-
this._privKey = opts.privKey
|
|
38
|
-
this._pubkeyHex = opts.pubkeyHex
|
|
39
|
-
this._endpoint = opts.endpoint
|
|
40
|
-
this._meshId = opts.meshId || '70016'
|
|
41
|
-
this._announceIntervalMs = opts.announceIntervalMs || 60000
|
|
42
|
-
this._maxAge = opts.maxAge || 300000
|
|
43
|
-
this._maxPeersResponse = opts.maxPeersResponse || 50
|
|
44
|
-
|
|
45
|
-
/** @type {Map<string, { pubkeyHex: string, endpoint: string, meshId: string, lastSeen: number }>} */
|
|
46
|
-
this._directory = new Map()
|
|
47
|
-
|
|
48
|
-
/** @type {Set<string>} recently seen announce hashes (dedup) */
|
|
49
|
-
this._seenAnnounces = new Set()
|
|
50
|
-
|
|
51
|
-
this._announceTimer = null
|
|
52
|
-
|
|
53
|
-
this.peerManager.on('peer:message', ({ pubkeyHex, message }) => {
|
|
54
|
-
this._handleMessage(pubkeyHex, message)
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Start periodic announcements.
|
|
60
|
-
*/
|
|
61
|
-
start () {
|
|
62
|
-
// Announce immediately
|
|
63
|
-
this._broadcastAnnounce()
|
|
64
|
-
// Then on interval
|
|
65
|
-
this._announceTimer = setInterval(() => {
|
|
66
|
-
this._broadcastAnnounce()
|
|
67
|
-
}, this._announceIntervalMs)
|
|
68
|
-
if (this._announceTimer.unref) this._announceTimer.unref()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Stop periodic announcements.
|
|
73
|
-
*/
|
|
74
|
-
stop () {
|
|
75
|
-
if (this._announceTimer) {
|
|
76
|
-
clearInterval(this._announceTimer)
|
|
77
|
-
this._announceTimer = null
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Request peer list from a specific peer.
|
|
83
|
-
* @param {string} pubkeyHex — peer to ask
|
|
84
|
-
*/
|
|
85
|
-
requestPeers (pubkeyHex) {
|
|
86
|
-
const conn = this.peerManager.peers.get(pubkeyHex)
|
|
87
|
-
if (conn) {
|
|
88
|
-
conn.send({ type: 'getpeers' })
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Request peer lists from all connected peers.
|
|
94
|
-
*/
|
|
95
|
-
requestPeersFromAll () {
|
|
96
|
-
this.peerManager.broadcast({ type: 'getpeers' })
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get the current peer directory.
|
|
101
|
-
* @returns {Array<{ pubkeyHex: string, endpoint: string, meshId: string, lastSeen: number }>}
|
|
102
|
-
*/
|
|
103
|
-
getDirectory () {
|
|
104
|
-
const now = Date.now()
|
|
105
|
-
const result = []
|
|
106
|
-
for (const [, entry] of this._directory) {
|
|
107
|
-
if (now - entry.lastSeen < this._maxAge) {
|
|
108
|
-
result.push({ ...entry })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return result
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get directory size (excluding stale entries).
|
|
116
|
-
* @returns {number}
|
|
117
|
-
*/
|
|
118
|
-
directorySize () {
|
|
119
|
-
return this.getDirectory().length
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Manually add a peer to the directory (e.g., seed peers from config).
|
|
124
|
-
* @param {{ pubkeyHex: string, endpoint: string, meshId?: string }} peer
|
|
125
|
-
*/
|
|
126
|
-
addSeed (peer) {
|
|
127
|
-
this._directory.set(peer.pubkeyHex, {
|
|
128
|
-
pubkeyHex: peer.pubkeyHex,
|
|
129
|
-
endpoint: peer.endpoint,
|
|
130
|
-
meshId: peer.meshId || this._meshId,
|
|
131
|
-
lastSeen: Date.now()
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import { signHash, verifyHash } from '@relay-federation/common/crypto'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GossipManager — peer discovery via WebSocket gossip protocol.
|
|
6
|
+
*
|
|
7
|
+
* Replaces HTTP-based registry scanning with pure P2P peer discovery.
|
|
8
|
+
* Three message types:
|
|
9
|
+
*
|
|
10
|
+
* getpeers — "tell me who you know"
|
|
11
|
+
* peers — response with list of known peers
|
|
12
|
+
* announce — "I'm alive" (signed, propagated to all peers)
|
|
13
|
+
*
|
|
14
|
+
* Announcements are signed with the bridge's private key to prevent
|
|
15
|
+
* impersonation. Each announcement includes a timestamp — stale
|
|
16
|
+
* announcements (older than maxAge) are discarded.
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* 'peer:discovered' — { pubkeyHex, endpoint, meshId }
|
|
20
|
+
* 'peers:response' — { peers: Array }
|
|
21
|
+
*/
|
|
22
|
+
export class GossipManager extends EventEmitter {
|
|
23
|
+
/**
|
|
24
|
+
* @param {import('./peer-manager.js').PeerManager} peerManager
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {import('@bsv/sdk').PrivateKey} opts.privKey — Bridge private key for signing
|
|
27
|
+
* @param {string} opts.pubkeyHex — Our compressed pubkey hex
|
|
28
|
+
* @param {string} opts.endpoint — Our advertised WSS endpoint
|
|
29
|
+
* @param {string} [opts.meshId='70016'] — Mesh identifier
|
|
30
|
+
* @param {number} [opts.announceIntervalMs=60000] — Re-announce interval
|
|
31
|
+
* @param {number} [opts.maxAge=300000] — Max age for announcements (5 min)
|
|
32
|
+
* @param {number} [opts.maxPeersResponse=50] — Max peers in a response
|
|
33
|
+
*/
|
|
34
|
+
constructor (peerManager, opts) {
|
|
35
|
+
super()
|
|
36
|
+
this.peerManager = peerManager
|
|
37
|
+
this._privKey = opts.privKey
|
|
38
|
+
this._pubkeyHex = opts.pubkeyHex
|
|
39
|
+
this._endpoint = opts.endpoint
|
|
40
|
+
this._meshId = opts.meshId || '70016'
|
|
41
|
+
this._announceIntervalMs = opts.announceIntervalMs || 60000
|
|
42
|
+
this._maxAge = opts.maxAge || 300000
|
|
43
|
+
this._maxPeersResponse = opts.maxPeersResponse || 50
|
|
44
|
+
|
|
45
|
+
/** @type {Map<string, { pubkeyHex: string, endpoint: string, meshId: string, lastSeen: number }>} */
|
|
46
|
+
this._directory = new Map()
|
|
47
|
+
|
|
48
|
+
/** @type {Set<string>} recently seen announce hashes (dedup) */
|
|
49
|
+
this._seenAnnounces = new Set()
|
|
50
|
+
|
|
51
|
+
this._announceTimer = null
|
|
52
|
+
|
|
53
|
+
this.peerManager.on('peer:message', ({ pubkeyHex, message }) => {
|
|
54
|
+
this._handleMessage(pubkeyHex, message)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start periodic announcements.
|
|
60
|
+
*/
|
|
61
|
+
start () {
|
|
62
|
+
// Announce immediately
|
|
63
|
+
this._broadcastAnnounce()
|
|
64
|
+
// Then on interval
|
|
65
|
+
this._announceTimer = setInterval(() => {
|
|
66
|
+
this._broadcastAnnounce()
|
|
67
|
+
}, this._announceIntervalMs)
|
|
68
|
+
if (this._announceTimer.unref) this._announceTimer.unref()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stop periodic announcements.
|
|
73
|
+
*/
|
|
74
|
+
stop () {
|
|
75
|
+
if (this._announceTimer) {
|
|
76
|
+
clearInterval(this._announceTimer)
|
|
77
|
+
this._announceTimer = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Request peer list from a specific peer.
|
|
83
|
+
* @param {string} pubkeyHex — peer to ask
|
|
84
|
+
*/
|
|
85
|
+
requestPeers (pubkeyHex) {
|
|
86
|
+
const conn = this.peerManager.peers.get(pubkeyHex)
|
|
87
|
+
if (conn) {
|
|
88
|
+
conn.send({ type: 'getpeers' })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Request peer lists from all connected peers.
|
|
94
|
+
*/
|
|
95
|
+
requestPeersFromAll () {
|
|
96
|
+
this.peerManager.broadcast({ type: 'getpeers' })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the current peer directory.
|
|
101
|
+
* @returns {Array<{ pubkeyHex: string, endpoint: string, meshId: string, lastSeen: number }>}
|
|
102
|
+
*/
|
|
103
|
+
getDirectory () {
|
|
104
|
+
const now = Date.now()
|
|
105
|
+
const result = []
|
|
106
|
+
for (const [, entry] of this._directory) {
|
|
107
|
+
if (now - entry.lastSeen < this._maxAge) {
|
|
108
|
+
result.push({ ...entry })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get directory size (excluding stale entries).
|
|
116
|
+
* @returns {number}
|
|
117
|
+
*/
|
|
118
|
+
directorySize () {
|
|
119
|
+
return this.getDirectory().length
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Manually add a peer to the directory (e.g., seed peers from config).
|
|
124
|
+
* @param {{ pubkeyHex: string, endpoint: string, meshId?: string }} peer
|
|
125
|
+
*/
|
|
126
|
+
addSeed (peer) {
|
|
127
|
+
this._directory.set(peer.pubkeyHex, {
|
|
128
|
+
pubkeyHex: peer.pubkeyHex,
|
|
129
|
+
endpoint: peer.endpoint,
|
|
130
|
+
meshId: peer.meshId || this._meshId,
|
|
131
|
+
lastSeen: Date.now()
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Update a peer's endpoint after successful handshake and broadcast the update.
|
|
137
|
+
*
|
|
138
|
+
* This enables self-healing when a peer changes IP: after they complete a
|
|
139
|
+
* cryptographically verified handshake from their new endpoint, we update
|
|
140
|
+
* our directory and immediately broadcast the change to all connected peers.
|
|
141
|
+
*
|
|
142
|
+
* @param {{ pubkeyHex: string, endpoint: string, meshId?: string }} peer
|
|
143
|
+
* @returns {boolean} true if the endpoint was updated (changed from cached value)
|
|
144
|
+
*/
|
|
145
|
+
updatePeerEndpoint (peer) {
|
|
146
|
+
const existing = this._directory.get(peer.pubkeyHex)
|
|
147
|
+
const endpointChanged = !existing || existing.endpoint !== peer.endpoint
|
|
148
|
+
|
|
149
|
+
// Update the directory entry
|
|
150
|
+
this._directory.set(peer.pubkeyHex, {
|
|
151
|
+
pubkeyHex: peer.pubkeyHex,
|
|
152
|
+
endpoint: peer.endpoint,
|
|
153
|
+
meshId: peer.meshId || existing?.meshId || this._meshId,
|
|
154
|
+
lastSeen: Date.now()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// If endpoint changed, broadcast the update to all connected peers
|
|
158
|
+
if (endpointChanged) {
|
|
159
|
+
const updatedPeer = this._directory.get(peer.pubkeyHex)
|
|
160
|
+
this.peerManager.broadcast({
|
|
161
|
+
type: 'peers',
|
|
162
|
+
peers: [updatedPeer]
|
|
163
|
+
}, peer.pubkeyHex) // exclude the peer we just updated
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return endpointChanged
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @private */
|
|
170
|
+
_handleMessage (pubkeyHex, message) {
|
|
171
|
+
switch (message.type) {
|
|
172
|
+
case 'getpeers':
|
|
173
|
+
this._onGetPeers(pubkeyHex)
|
|
174
|
+
break
|
|
175
|
+
case 'peers':
|
|
176
|
+
this._onPeers(pubkeyHex, message)
|
|
177
|
+
break
|
|
178
|
+
case 'announce':
|
|
179
|
+
this._onAnnounce(pubkeyHex, message)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @private */
|
|
185
|
+
_onGetPeers (pubkeyHex) {
|
|
186
|
+
const peers = this.getDirectory()
|
|
187
|
+
.filter(p => p.pubkeyHex !== pubkeyHex) // don't send them back to themselves
|
|
188
|
+
.slice(0, this._maxPeersResponse)
|
|
189
|
+
|
|
190
|
+
const conn = this.peerManager.peers.get(pubkeyHex)
|
|
191
|
+
if (conn) {
|
|
192
|
+
conn.send({ type: 'peers', peers })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @private */
|
|
197
|
+
_onPeers (pubkeyHex, message) {
|
|
198
|
+
if (!Array.isArray(message.peers)) return
|
|
199
|
+
|
|
200
|
+
for (const peer of message.peers) {
|
|
201
|
+
if (!peer.pubkeyHex || !peer.endpoint) continue
|
|
202
|
+
if (peer.pubkeyHex === this._pubkeyHex) continue // skip self
|
|
203
|
+
|
|
204
|
+
const isNew = !this._directory.has(peer.pubkeyHex)
|
|
205
|
+
|
|
206
|
+
this._directory.set(peer.pubkeyHex, {
|
|
207
|
+
pubkeyHex: peer.pubkeyHex,
|
|
208
|
+
endpoint: peer.endpoint,
|
|
209
|
+
meshId: peer.meshId || 'unknown',
|
|
210
|
+
lastSeen: Date.now()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if (isNew) {
|
|
214
|
+
this.emit('peer:discovered', {
|
|
215
|
+
pubkeyHex: peer.pubkeyHex,
|
|
216
|
+
endpoint: peer.endpoint,
|
|
217
|
+
meshId: peer.meshId || 'unknown'
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.emit('peers:response', { peers: message.peers, from: pubkeyHex })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @private */
|
|
226
|
+
_onAnnounce (sourcePubkey, message) {
|
|
227
|
+
if (!message.pubkeyHex || !message.endpoint || !message.timestamp || !message.signature) return
|
|
228
|
+
|
|
229
|
+
// Dedup — don't process the same announcement twice
|
|
230
|
+
const announceId = `${message.pubkeyHex}:${message.timestamp}`
|
|
231
|
+
if (this._seenAnnounces.has(announceId)) return
|
|
232
|
+
this._seenAnnounces.add(announceId)
|
|
233
|
+
|
|
234
|
+
// Trim dedup set if it gets too large
|
|
235
|
+
if (this._seenAnnounces.size > 10000) {
|
|
236
|
+
const arr = [...this._seenAnnounces]
|
|
237
|
+
this._seenAnnounces = new Set(arr.slice(arr.length - 5000))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check age
|
|
241
|
+
const age = Date.now() - message.timestamp
|
|
242
|
+
if (age > this._maxAge || age < -30000) return // too old or too far in the future
|
|
243
|
+
|
|
244
|
+
// Verify signature
|
|
245
|
+
const payload = `${message.pubkeyHex}:${message.endpoint}:${message.meshId || ''}:${message.timestamp}`
|
|
246
|
+
const dataHex = Buffer.from(payload, 'utf8').toString('hex')
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const valid = verifyHash(dataHex, message.signature, message.pubkeyHex)
|
|
250
|
+
if (!valid) return
|
|
251
|
+
} catch {
|
|
252
|
+
return // invalid signature format
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Skip self
|
|
256
|
+
if (message.pubkeyHex === this._pubkeyHex) return
|
|
257
|
+
|
|
258
|
+
const isNew = !this._directory.has(message.pubkeyHex)
|
|
259
|
+
|
|
260
|
+
this._directory.set(message.pubkeyHex, {
|
|
261
|
+
pubkeyHex: message.pubkeyHex,
|
|
262
|
+
endpoint: message.endpoint,
|
|
263
|
+
meshId: message.meshId || 'unknown',
|
|
264
|
+
lastSeen: Date.now()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (isNew) {
|
|
268
|
+
this.emit('peer:discovered', {
|
|
269
|
+
pubkeyHex: message.pubkeyHex,
|
|
270
|
+
endpoint: message.endpoint,
|
|
271
|
+
meshId: message.meshId || 'unknown'
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Re-broadcast to all peers except source
|
|
276
|
+
this.peerManager.broadcast(message, sourcePubkey)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** @private */
|
|
280
|
+
_broadcastAnnounce () {
|
|
281
|
+
const timestamp = Date.now()
|
|
282
|
+
const payload = `${this._pubkeyHex}:${this._endpoint}:${this._meshId}:${timestamp}`
|
|
283
|
+
const dataHex = Buffer.from(payload, 'utf8').toString('hex')
|
|
284
|
+
const signature = signHash(dataHex, this._privKey)
|
|
285
|
+
|
|
286
|
+
const message = {
|
|
287
|
+
type: 'announce',
|
|
288
|
+
pubkeyHex: this._pubkeyHex,
|
|
289
|
+
endpoint: this._endpoint,
|
|
290
|
+
meshId: this._meshId,
|
|
291
|
+
timestamp,
|
|
292
|
+
signature
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add to our own dedup set
|
|
296
|
+
this._seenAnnounces.add(`${this._pubkeyHex}:${timestamp}`)
|
|
297
|
+
|
|
298
|
+
this.peerManager.broadcast(message)
|
|
299
|
+
}
|
|
300
|
+
}
|