@steve02081504/fount-p2p 0.0.14 → 0.0.16

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.
@@ -0,0 +1,280 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { randomBytes } from 'node:crypto'
3
+
4
+ import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
5
+ import { normalizeHex64 } from '../../core/hexIds.mjs'
6
+ import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
7
+ import { mergeSignalingRelayUrls } from '../../discovery/nostr.mjs'
8
+ import { getNodeTransportSettings } from '../../node/identity.mjs'
9
+ import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
10
+ import { ms } from '../../utils/duration.mjs'
11
+ import { createLruMap } from '../../utils/lru.mjs'
12
+ import { asLinkHandle } from '../pipe.mjs'
13
+
14
+ import { LINK_LEVEL_NOSTR } from './levels.mjs'
15
+ import { createLinkIdBoundPipe } from './link_id_pipe.mjs'
16
+
17
+ /** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。 */
18
+ const MAX_LINK_PAYLOAD_CHARS = 12 * 1024
19
+ /** open 到达前为同一 linkId 暂存的 c/b 包上限。 */
20
+ const PENDING_PACKETS_MAX = 32
21
+ /** Nostr 链握手超时(relay RTT 更慢)。 */
22
+ const NOSTR_HANDSHAKE_TIMEOUT_MS = ms('30s')
23
+ /** Nostr 链心跳间隔。 */
24
+ const NOSTR_HEARTBEAT_MS = ms('60s')
25
+ /** Nostr 链空闲超时。 */
26
+ const NOSTR_IDLE_TIMEOUT_MS = ms('3m')
27
+
28
+ /**
29
+ * @returns {string[]} 当前可用中继 URL
30
+ */
31
+ function resolveDefaultRelayUrls() {
32
+ return getSignalingRuntimeConfig().relayOverride
33
+ ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
34
+ }
35
+
36
+ /**
37
+ * @param {string} remoteNodeHash 对端
38
+ * @param {object} packet link 包
39
+ * @returns {Promise<void>}
40
+ */
41
+ async function publishLinkPacket(remoteNodeHash, packet) {
42
+ await sendNodeSignalPacket(remoteNodeHash, packet)
43
+ }
44
+
45
+ /**
46
+ * 创建 Nostr 末位数据链路 provider(level = -∞)。
47
+ * @param {{ getRelayUrls?: () => string[] }} [options] 中继解析(测试可注入)
48
+ * @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
49
+ */
50
+ export function createNostrLinkProvider(options = {}) {
51
+ const resolveRelayUrls = typeof options.getRelayUrls === 'function'
52
+ ? options.getRelayUrls
53
+ : resolveDefaultRelayUrls
54
+
55
+ /** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
56
+ let onInbound = null
57
+ /** @type {object | null} */
58
+ let localIdentity = null
59
+ /**
60
+ * @typedef {{
61
+ * pipe: ReturnType<typeof createLinkIdBoundPipe>,
62
+ * remoteNodeHash: string,
63
+ * initiator: boolean,
64
+ * }} NostrLinkSession
65
+ */
66
+ /** @type {Map<string, NostrLinkSession>} */
67
+ const sessions = new Map()
68
+ /** @type {Map<string, object[]> & { touch: (key: string, value: object[]) => void }} */
69
+ const pendingByLinkId = createLruMap(64)
70
+
71
+ /**
72
+ * @returns {boolean} discovery nostr 已注册且有中继
73
+ */
74
+ function isAvailable() {
75
+ if (!getDiscoveryProvider('nostr')) return false
76
+ return resolveRelayUrls().length > 0
77
+ }
78
+
79
+ /**
80
+ * @param {NostrLinkSession} session 会话
81
+ * @param {object} packet link 包
82
+ * @returns {void}
83
+ */
84
+ function applySessionPacket(session, packet) {
85
+ const op = String(packet.op || '')
86
+ if (op === 'close') {
87
+ void session.pipe.close('remote-close')
88
+ return
89
+ }
90
+ if (op === 'c') {
91
+ if (typeof packet.payload !== 'string') return
92
+ session.pipe.handleInbound(packet.payload)
93
+ return
94
+ }
95
+ if (op === 'b') {
96
+ if (typeof packet.payload !== 'string') return
97
+ try {
98
+ session.pipe.handleInbound(Buffer.from(base64ToBytes(packet.payload)))
99
+ }
100
+ catch { /* drop malformed */ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * @param {string} linkId 链路 id
106
+ * @param {object} packet 暂存包
107
+ * @returns {void}
108
+ */
109
+ function bufferPending(linkId, packet) {
110
+ let list = pendingByLinkId.get(linkId)
111
+ if (!list) {
112
+ list = []
113
+ pendingByLinkId.touch(linkId, list)
114
+ }
115
+ if (list.length >= PENDING_PACKETS_MAX) list.shift()
116
+ list.push(packet)
117
+ }
118
+
119
+ /**
120
+ * @param {string} remoteNodeHash 对端
121
+ * @param {string} linkId 链路 id
122
+ * @param {string} op open|c|b|close
123
+ * @param {string} [payload] 可选载荷
124
+ * @returns {Promise<void>}
125
+ */
126
+ async function sendOp(remoteNodeHash, linkId, op, payload) {
127
+ if (payload != null && payload.length > MAX_LINK_PAYLOAD_CHARS)
128
+ throw new Error('p2p: nostr link payload too large')
129
+ const packet = {
130
+ type: 'link',
131
+ op,
132
+ from: localIdentity?.nodeHash || '',
133
+ linkId,
134
+ }
135
+ if (payload != null) packet.payload = payload
136
+ await publishLinkPacket(remoteNodeHash, packet)
137
+ }
138
+
139
+ /**
140
+ * @param {object} opts 会话选项
141
+ * @returns {ReturnType<typeof createLinkIdBoundPipe>} pipe
142
+ */
143
+ function openPipe(opts) {
144
+ const { linkId, remoteNodeHash, initiator } = opts
145
+ const pipe = createLinkIdBoundPipe({
146
+ providerId: 'nostr',
147
+ level: LINK_LEVEL_NOSTR,
148
+ initiator: !!initiator,
149
+ linkId,
150
+ nodeHash: remoteNodeHash,
151
+ localIdentity: opts.localIdentity || localIdentity,
152
+ handshakeTimeoutMs: NOSTR_HANDSHAKE_TIMEOUT_MS,
153
+ heartbeatMs: NOSTR_HEARTBEAT_MS,
154
+ idleTimeoutMs: NOSTR_IDLE_TIMEOUT_MS,
155
+ /**
156
+ * @param {string} text control JSON
157
+ * @returns {Promise<void>}
158
+ */
159
+ async sendControlText(text) {
160
+ await sendOp(remoteNodeHash, linkId, 'c', text)
161
+ },
162
+ /**
163
+ * @param {string} _action action
164
+ * @param {Uint8Array} frame 帧
165
+ * @returns {Promise<void>}
166
+ */
167
+ async sendFrame(_action, frame) {
168
+ await sendOp(remoteNodeHash, linkId, 'b', bytesToBase64(frame))
169
+ },
170
+ /**
171
+ * @returns {Promise<void>}
172
+ */
173
+ async closeTransport() {
174
+ sessions.delete(linkId)
175
+ try {
176
+ await sendOp(remoteNodeHash, linkId, 'close')
177
+ }
178
+ catch { /* ignore */ }
179
+ },
180
+ })
181
+ sessions.set(linkId, { pipe, remoteNodeHash, initiator: !!initiator })
182
+ pipe.onDown(() => { sessions.delete(linkId) })
183
+ return pipe
184
+ }
185
+
186
+ /**
187
+ * 入站已解密的 link 包(由信令 demux 调用)。
188
+ * @param {object} packet link 包
189
+ * @returns {void}
190
+ */
191
+ function deliverPacket(packet) {
192
+ if (packet?.type !== 'link') return
193
+ const linkId = normalizeHex64(packet.linkId)
194
+ const from = normalizeHex64(packet.from)
195
+ if (!linkId || !from) return
196
+ if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
197
+
198
+ const op = String(packet.op || '')
199
+ if (op === 'open') {
200
+ if (sessions.has(linkId)) return
201
+ if (!onInbound || !localIdentity) return
202
+ const pipe = openPipe({
203
+ linkId,
204
+ remoteNodeHash: from,
205
+ initiator: false,
206
+ localIdentity,
207
+ })
208
+ onInbound(asLinkHandle(pipe))
209
+ void pipe.startHandshake().catch(() => {
210
+ sessions.delete(linkId)
211
+ void pipe.close('accept-failed')
212
+ })
213
+ const pending = pendingByLinkId.get(linkId) || []
214
+ pendingByLinkId.delete(linkId)
215
+ const session = sessions.get(linkId)
216
+ if (session)
217
+ for (const queued of pending)
218
+ applySessionPacket(session, queued)
219
+ return
220
+ }
221
+
222
+ const session = sessions.get(linkId)
223
+ if (!session) {
224
+ if (op === 'c' || op === 'b' || op === 'close')
225
+ bufferPending(linkId, packet)
226
+ return
227
+ }
228
+ applySessionPacket(session, packet)
229
+ }
230
+
231
+ return {
232
+ id: 'nostr',
233
+ level: LINK_LEVEL_NOSTR,
234
+ caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'sync' },
235
+ isAvailable,
236
+ /**
237
+ * @returns {boolean} 有中继即可
238
+ */
239
+ canReach() {
240
+ return isAvailable()
241
+ },
242
+ deliverPacket,
243
+ /**
244
+ * @param {object} dialOptions dial 选项
245
+ * @returns {Promise<import('./index.mjs').LinkHandle | null>} link
246
+ */
247
+ async dial(dialOptions) {
248
+ if (!isAvailable()) return null
249
+ const remoteNodeHash = normalizeHex64(dialOptions.nodeHash)
250
+ if (!remoteNodeHash) return null
251
+ localIdentity = dialOptions.localIdentity || localIdentity
252
+ if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
253
+ const linkId = randomBytes(32).toString('hex')
254
+ const pipe = openPipe({
255
+ linkId,
256
+ remoteNodeHash,
257
+ initiator: true,
258
+ localIdentity,
259
+ })
260
+ await sendOp(remoteNodeHash, linkId, 'open')
261
+ await pipe.startHandshake()
262
+ return asLinkHandle(pipe)
263
+ },
264
+ /**
265
+ * @param {{ onInbound: (link: import('./index.mjs').LinkHandle) => void, localIdentity: object }} handlers 回调
266
+ * @returns {() => void} 停止 listening
267
+ */
268
+ ensureListening(handlers) {
269
+ onInbound = handlers.onInbound
270
+ localIdentity = handlers.localIdentity
271
+ return () => {
272
+ onInbound = null
273
+ for (const session of sessions.values())
274
+ void session.pipe.close('listen-stop')
275
+ sessions.clear()
276
+ pendingByLinkId.clear()
277
+ }
278
+ },
279
+ }
280
+ }
@@ -34,6 +34,13 @@ function formatErrorReason(error) {
34
34
 
35
35
  let cachedAvailable = null
36
36
 
37
+ /**
38
+ * @returns {boolean} 是否跑在 Deno
39
+ */
40
+ function isDenoRuntime() {
41
+ return typeof globalThis.Deno !== 'undefined'
42
+ }
43
+
37
44
  /**
38
45
  * 探测 WebRTC(node-datachannel)是否可用。
39
46
  * @returns {Promise<boolean>} 可用为 true
@@ -46,7 +53,12 @@ export async function canUseWebRtcLink() {
46
53
  }
47
54
  catch (error) {
48
55
  cachedAvailable = false
49
- nodeDebug('p2p:webrtc unavailable', { err: formatErrorReason(error) })
56
+ const reason = formatErrorReason(error)
57
+ nodeDebug('p2p:webrtc unavailable', {
58
+ err: isDenoRuntime()
59
+ ? `${reason} | deno: see docs/runtime.md (node-datachannel scripts only)`
60
+ : reason,
61
+ })
50
62
  }
51
63
  return cachedAvailable
52
64
  }
package/link/rtc.mjs CHANGED
@@ -4,15 +4,23 @@ import { toBytes } from '../core/bytes_codec.mjs'
4
4
  import { getSignalingRuntimeConfig } from '../node/instance.mjs'
5
5
  import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
6
6
 
7
+ /** @type {boolean} */
8
+ let exitCleanupHooked = false
9
+
7
10
  /**
8
- * 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
11
+ * 注册进程退出时销毁 libdatachannel 原生资源(首次成功加载后挂一次)。
9
12
  * libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
10
13
  * Windows 上会触发堆损坏(退出码 0xC0000374)。
14
+ * @returns {Promise<void>}
11
15
  */
12
- const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
13
- process.on('exit', () => {
14
- try { cleanup?.() } catch { /* already torn down */ }
15
- })
16
+ async function ensureNodeDatachannelExitCleanup() {
17
+ if (exitCleanupHooked) return
18
+ exitCleanupHooked = true
19
+ const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
20
+ process.on('exit', () => {
21
+ try { cleanup?.() } catch { /* already torn down */ }
22
+ })
23
+ }
16
24
 
17
25
  /**
18
26
  * 加载 node-datachannel polyfill,并按配置包装 RTCPeerConnection。
@@ -20,6 +28,7 @@ process.on('exit', () => {
20
28
  */
21
29
  export async function loadNodeRtcPolyfill() {
22
30
  const mod = await import('node-datachannel/polyfill')
31
+ await ensureNodeDatachannelExitCleanup()
23
32
  const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
24
33
  return {
25
34
  RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -3,11 +3,13 @@ import { Buffer } from 'node:buffer'
3
3
  import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
4
4
  import { keyPairFromSeed } from '../crypto/crypto.mjs'
5
5
  import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
6
+ import { setDiscoveryPeerClueListener } from '../discovery/peer_clue.mjs'
6
7
  import { listLinkProviders } from '../link/providers/index.mjs'
7
8
  import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
8
9
  import { nodeDebug, shortHash } from '../node/log.mjs'
9
10
  import { loadPeerPoolView } from '../node/network.mjs'
10
11
  import { createOverlayRouter } from '../overlay/index.mjs'
12
+ import { ms } from '../utils/duration.mjs'
11
13
  import { emitSafe } from '../utils/emit_safe.mjs'
12
14
  import { createLruMap } from '../utils/lru.mjs'
13
15
 
@@ -18,6 +20,11 @@ import { createOfferAnswerDial } from './offer_answer.mjs'
18
20
  import { pickMeshEvictionVictim } from './peer_pool.mjs'
19
21
  import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
20
22
 
23
+ /** dial exhausted 后的初始冷却。 */
24
+ const DIAL_COOLDOWN_BASE_MS = ms('30s')
25
+ /** dial 冷却上限。 */
26
+ const DIAL_COOLDOWN_MAX_MS = ms('10m')
27
+
21
28
  /**
22
29
  * 解析或从节点种子推导本地身份。
23
30
  * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array } | undefined} localIdentity 可选的预置身份
@@ -84,6 +91,8 @@ export function createLinkRegistry(options = {}) {
84
91
  const links = new Map()
85
92
  /** @type {Map<string, Promise<object | null>>} */
86
93
  const inflights = new Map()
94
+ /** @type {Map<string, { until: number, failures: number }> & { touch: (key: string, value: { until: number, failures: number }) => void }} */
95
+ const dialCooldown = createLruMap(1024)
87
96
  /** @type {Map<string, ReturnType<typeof import('./offer_answer.mjs').createBufferedSignalSession>>} */
88
97
  const signalSessions = new Map()
89
98
  /** @type {Map<string, Set<string>>} */
@@ -265,6 +274,19 @@ export function createLinkRegistry(options = {}) {
265
274
  if (!normalized || normalized === localIdentity.nodeHash) return null
266
275
  if (links.has(normalized)) return links.get(normalized)
267
276
  if (inflights.has(normalized)) return await inflights.get(normalized)
277
+ const cool = dialCooldown.get(normalized)
278
+ const now = Date.now()
279
+ if (cool) {
280
+ if (now < cool.until) {
281
+ nodeDebug('p2p:dial cooldown', {
282
+ peer: shortHash(normalized),
283
+ failures: cool.failures,
284
+ retryInMs: cool.until - now,
285
+ })
286
+ return null
287
+ }
288
+ dialCooldown.delete(normalized)
289
+ }
268
290
  const task = (async () => {
269
291
  nodeDebug('p2p:dial start', { peer: shortHash(normalized) })
270
292
  await prepareConnectToNode(normalized)
@@ -289,6 +311,7 @@ export function createLinkRegistry(options = {}) {
289
311
  if (provider.caps?.needsOfferAnswer) {
290
312
  const link = await dialOfferAnswer(provider, normalized)
291
313
  if (link) {
314
+ dialCooldown.delete(normalized)
292
315
  nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
293
316
  return link
294
317
  }
@@ -297,6 +320,7 @@ export function createLinkRegistry(options = {}) {
297
320
  }
298
321
  const link = await dialProvider(provider, normalized)
299
322
  if (link) {
323
+ dialCooldown.delete(normalized)
300
324
  nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
301
325
  return link
302
326
  }
@@ -310,7 +334,10 @@ export function createLinkRegistry(options = {}) {
310
334
  })
311
335
  }
312
336
 
313
- nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized) })
337
+ const failures = (cool?.failures || 0) + 1
338
+ const delay = Math.min(DIAL_COOLDOWN_MAX_MS, DIAL_COOLDOWN_BASE_MS * (2 ** Math.min(failures - 1, 5)))
339
+ dialCooldown.touch(normalized, { until: Date.now() + delay, failures })
340
+ nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized), cooldownMs: delay, failures })
314
341
  return null
315
342
  })().finally(() => inflights.delete(normalized))
316
343
  inflights.set(normalized, task)
@@ -351,6 +378,12 @@ export function createLinkRegistry(options = {}) {
351
378
  })
352
379
  exploreLinkHashes = meshKeepalive.exploreLinkHashes
353
380
  setDiscoveryLinkDialer(ensureDirectLinkToNode)
381
+ setDiscoveryPeerClueListener(nodeHash => {
382
+ const hash = normalizeHex64(nodeHash)
383
+ if (!hash) return
384
+ dialCooldown.delete(hash)
385
+ recentAdverts.touch(hash, Date.now())
386
+ })
354
387
 
355
388
  /**
356
389
  *
@@ -619,6 +652,7 @@ export function createLinkRegistry(options = {}) {
619
652
  return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
620
653
  applyAdvertPeerHints(verifiedNodeHash, body, meta)
621
654
  recentAdverts.touch(verifiedNodeHash, Date.now())
655
+ dialCooldown.delete(verifiedNodeHash)
622
656
  await Promise.resolve(onAdvert(verifiedNodeHash, body))
623
657
  })
624
658
  },
@@ -631,6 +665,7 @@ export function createLinkRegistry(options = {}) {
631
665
  async shutdown() {
632
666
  await meshKeepalive?.stop()
633
667
  setDiscoveryLinkDialer(null)
668
+ setDiscoveryPeerClueListener(null)
634
669
  await bootstrap.shutdown()
635
670
  overlayRouter?.close()
636
671
  overlayRouter = null
@@ -638,6 +673,7 @@ export function createLinkRegistry(options = {}) {
638
673
  await link.close('registry-shutdown')
639
674
  links.clear()
640
675
  inflights.clear()
676
+ dialCooldown.clear()
641
677
  for (const session of signalSessions.values()) session.clear()
642
678
  signalSessions.clear()
643
679
  },
@@ -150,11 +150,10 @@ export function createOfferAnswerDial(deps) {
150
150
  }
151
151
 
152
152
  /**
153
- * @param {Uint8Array} bytes 加密信令
153
+ * @param {object} packet 已解密的 signal 包
154
154
  * @returns {Promise<void>}
155
155
  */
156
- async function handleIncomingSignal(bytes) {
157
- const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
156
+ async function handleSignalPacket(packet) {
158
157
  if (packet?.type !== 'signal') return
159
158
  const remoteNodeHash = normalizeHex64(packet.from)
160
159
  const connId = String(packet.connId || '')
@@ -175,6 +174,21 @@ export function createOfferAnswerDial(deps) {
175
174
  session.deliver(packet.body)
176
175
  }
177
176
 
177
+ /**
178
+ * @param {Uint8Array} bytes 加密信令
179
+ * @returns {Promise<void>}
180
+ */
181
+ async function handleIncomingSignal(bytes) {
182
+ const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
183
+ if (!packet) return
184
+ if (packet.type === 'link') {
185
+ const provider = listLinkProviders().find(entry => entry.id === 'nostr')
186
+ provider?.deliverPacket?.(packet)
187
+ return
188
+ }
189
+ await handleSignalPacket(packet)
190
+ }
191
+
178
192
  /**
179
193
  * @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
180
194
  * @param {string} remoteNodeHash 远端 nodeHash
@@ -187,5 +201,5 @@ export function createOfferAnswerDial(deps) {
187
201
  return await buildConnLink({ provider, remoteNodeHash, connId, session, initiator: true })
188
202
  }
189
203
 
190
- return { handleIncomingSignal, dialOfferAnswer }
204
+ return { handleIncomingSignal, handleSignalPacket, dialOfferAnswer }
191
205
  }
@@ -18,6 +18,7 @@ import {
18
18
  unregisterLinkProvider,
19
19
  } from '../link/providers/index.mjs'
20
20
  import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
21
+ import { createNostrLinkProvider } from '../link/providers/nostr.mjs'
21
22
  import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
22
23
  import { getNodeTransportSettings } from '../node/identity.mjs'
23
24
  import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
@@ -116,7 +117,9 @@ export function createRuntimeBootstrap(deps) {
116
117
  function registerDiscoveryDefaults() {
117
118
  const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
118
119
  if (!providerIds.has('lan'))
119
- registerDiscoveryProvider(createLanDiscoveryProvider())
120
+ registerDiscoveryProvider(createLanDiscoveryProvider({
121
+ localNodeHash: localIdentity.nodeHash,
122
+ }))
120
123
  if (!providerIds.has('nostr'))
121
124
  registerNostrProvider()
122
125
  }
@@ -128,6 +131,7 @@ export function createRuntimeBootstrap(deps) {
128
131
  unregisterDiscoveryProvider('nostr')
129
132
  registerDiscoveryProvider(createNostrDiscoveryProvider({
130
133
  getRelayUrls: resolveNostrRelayUrls,
134
+ localNodeHash: localIdentity.nodeHash,
131
135
  }))
132
136
  }
133
137
 
@@ -154,6 +158,8 @@ export function createRuntimeBootstrap(deps) {
154
158
  const ids = new Set(listLinkProviders().map(provider => provider.id))
155
159
  if (!ids.has('webrtc'))
156
160
  registerLinkProvider(createWebRtcLinkProvider())
161
+ if (!ids.has('nostr'))
162
+ registerLinkProvider(createNostrLinkProvider({ getRelayUrls: resolveNostrRelayUrls }))
157
163
  }
158
164
 
159
165
  /**