@steve02081504/fount-p2p 0.0.12 → 0.0.13
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/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
package/discovery/nostr.mjs
CHANGED
|
@@ -4,7 +4,17 @@ import { schnorr } from '@noble/curves/secp256k1.js'
|
|
|
4
4
|
import WebSocket from 'ws'
|
|
5
5
|
|
|
6
6
|
import { base64ToBytes, hexToBytes, bytesToBase64, bytesToHex } from '../core/bytes_codec.mjs'
|
|
7
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
7
8
|
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
9
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
10
|
+
|
|
11
|
+
import { ingestEncryptedAdvert } from './adverts.mjs'
|
|
12
|
+
import {
|
|
13
|
+
encryptSignalPacket,
|
|
14
|
+
groupRendezvousKey,
|
|
15
|
+
networkRendezvousKey,
|
|
16
|
+
nodeRendezvousKey,
|
|
17
|
+
} from './internal/signal_crypto.mjs'
|
|
8
18
|
|
|
9
19
|
/** 默认 Nostr 中继 URL 列表。 */
|
|
10
20
|
export const DEFAULT_RELAY_URLS = [
|
|
@@ -23,9 +33,119 @@ export const NOSTR_ADVERT_KIND = 27235
|
|
|
23
33
|
/** Nostr signal 事件 kind。 */
|
|
24
34
|
export const NOSTR_SIGNAL_KIND = 27236
|
|
25
35
|
|
|
36
|
+
const ADVERT_TTL_MS = 10 * 60_000
|
|
37
|
+
|
|
38
|
+
/** @type {Map<string, number>} 网络域 nodeHash → lastSeenAt */
|
|
39
|
+
const visibleByHash = new Map()
|
|
40
|
+
/** @type {Map<string, Map<string, number>>} roomSecret → (nodeHash → lastSeenAt) */
|
|
41
|
+
const visibleByGroup = new Map()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Map<string, number>} pool 可见池
|
|
45
|
+
* @param {number} now 当前时间
|
|
46
|
+
* @param {number} ttlMs TTL
|
|
47
|
+
* @returns {string[]} 未过期 nodeHash
|
|
48
|
+
*/
|
|
49
|
+
function listPoolHashes(pool, now, ttlMs) {
|
|
50
|
+
/** @type {string[]} */
|
|
51
|
+
const out = []
|
|
52
|
+
for (const [hash, seenAt] of pool)
|
|
53
|
+
if (now - seenAt <= ttlMs) out.push(hash)
|
|
54
|
+
else pool.delete(hash)
|
|
55
|
+
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 写入网络域可见池(非群)。
|
|
61
|
+
* @param {string} nodeHash 节点 hash
|
|
62
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
63
|
+
* @returns {void}
|
|
64
|
+
*/
|
|
65
|
+
export function noteNostrVisibleNode(nodeHash, now = Date.now()) {
|
|
66
|
+
const hash = normalizeHex64(nodeHash)
|
|
67
|
+
if (!isHex64(hash)) return
|
|
68
|
+
visibleByHash.set(hash, now)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 写入群域可见池(与网络域隔离)。
|
|
73
|
+
* @param {string} roomSecret 房间密钥
|
|
74
|
+
* @param {string} nodeHash 节点 hash
|
|
75
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
export function noteNostrGroupVisibleNode(roomSecret, nodeHash, now = Date.now()) {
|
|
79
|
+
const key = String(roomSecret || '')
|
|
80
|
+
const hash = normalizeHex64(nodeHash)
|
|
81
|
+
if (!key || !isHex64(hash)) return
|
|
82
|
+
let pool = visibleByGroup.get(key)
|
|
83
|
+
if (!pool) {
|
|
84
|
+
pool = new Map()
|
|
85
|
+
visibleByGroup.set(key, pool)
|
|
86
|
+
}
|
|
87
|
+
pool.set(hash, now)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
92
|
+
* @param {number} [ttlMs=ADVERT_TTL_MS] TTL
|
|
93
|
+
* @returns {string[]} 网络域可见 nodeHash
|
|
94
|
+
*/
|
|
95
|
+
export function listNostrVisibleNodeHashes(now = Date.now(), ttlMs = ADVERT_TTL_MS) {
|
|
96
|
+
return listPoolHashes(visibleByHash, now, ttlMs)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} roomSecret 房间密钥
|
|
101
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
102
|
+
* @param {number} [ttlMs=ADVERT_TTL_MS] TTL
|
|
103
|
+
* @returns {string[]} 该群可见 nodeHash
|
|
104
|
+
*/
|
|
105
|
+
export function listNostrGroupVisibleNodeHashes(roomSecret, now = Date.now(), ttlMs = ADVERT_TTL_MS) {
|
|
106
|
+
const key = String(roomSecret || '')
|
|
107
|
+
const pool = visibleByGroup.get(key)
|
|
108
|
+
if (!pool) return []
|
|
109
|
+
const out = listPoolHashes(pool, now, ttlMs)
|
|
110
|
+
if (!pool.size) visibleByGroup.delete(key)
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 解密并验签后写入 Nostr 可见池;伪造 body.nodeHash 无效。
|
|
116
|
+
* @param {string} rendezvousKey rendezvous 键
|
|
117
|
+
* @param {Uint8Array} bytes 加密 advert
|
|
118
|
+
* @param {{ roomSecret?: string }} [options] 带 roomSecret 时写入群池
|
|
119
|
+
* @returns {Promise<string | null>} 验签通过的 nodeHash
|
|
120
|
+
*/
|
|
121
|
+
export async function acceptNostrAdvert(rendezvousKey, bytes, options = {}) {
|
|
122
|
+
const ingested = await ingestEncryptedAdvert(rendezvousKey, bytes)
|
|
123
|
+
if (!ingested) return null
|
|
124
|
+
const hash = ingested.verifiedNodeHash
|
|
125
|
+
const roomSecret = options.roomSecret
|
|
126
|
+
let firstSeen = true
|
|
127
|
+
if (roomSecret) {
|
|
128
|
+
const key = String(roomSecret || '')
|
|
129
|
+
firstSeen = !visibleByGroup.get(key)?.has(hash)
|
|
130
|
+
noteNostrGroupVisibleNode(key, hash)
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
firstSeen = !visibleByHash.has(hash)
|
|
134
|
+
noteNostrVisibleNode(hash)
|
|
135
|
+
}
|
|
136
|
+
if (firstSeen)
|
|
137
|
+
nodeDebug('p2p:nostr peer visible', { peer: shortHash(hash), group: !!roomSecret })
|
|
138
|
+
return hash
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @returns {void} 测试用 */
|
|
142
|
+
export function clearNostrVisibleNodes() {
|
|
143
|
+
visibleByHash.clear()
|
|
144
|
+
visibleByGroup.clear()
|
|
145
|
+
}
|
|
146
|
+
|
|
26
147
|
/**
|
|
27
148
|
* 关掉 WebSocket:已连上则先 close,grace 内未 CLOSED 再 terminate。
|
|
28
|
-
* 连接中直接 terminate(无握手可优雅收尾)。
|
|
29
149
|
* @param {import('ws').WebSocket} ws 连接
|
|
30
150
|
* @returns {void}
|
|
31
151
|
*/
|
|
@@ -39,7 +159,6 @@ function dropWebSocket(ws) {
|
|
|
39
159
|
if (ws.readyState !== WebSocket.CLOSED)
|
|
40
160
|
try { ws.terminate() } catch { /* ignore */ }
|
|
41
161
|
}, NOSTR_CLOSE_GRACE_MS)
|
|
42
|
-
// Grace is polite to the peer; must not pin the process after shutdown.
|
|
43
162
|
timer.unref()
|
|
44
163
|
ws.once('close', () => clearTimeout(timer))
|
|
45
164
|
try {
|
|
@@ -52,7 +171,6 @@ function dropWebSocket(ws) {
|
|
|
52
171
|
}
|
|
53
172
|
|
|
54
173
|
/**
|
|
55
|
-
* 去重并清洗中继 URL 列表。
|
|
56
174
|
* @param {string[] | undefined | null} urls 原始列表
|
|
57
175
|
* @returns {string[]} 清洗后的列表
|
|
58
176
|
*/
|
|
@@ -64,7 +182,6 @@ function dedupeRelayUrls(urls) {
|
|
|
64
182
|
}
|
|
65
183
|
|
|
66
184
|
/**
|
|
67
|
-
* 合并默认与用户配置的中继 URL(去重)。
|
|
68
185
|
* @param {string[] | undefined | null} userRelayUrls 用户自定义中继列表
|
|
69
186
|
* @returns {string[]} 合并后的中继 URL 列表
|
|
70
187
|
*/
|
|
@@ -74,7 +191,6 @@ export function mergeSignalingRelayUrls(userRelayUrls) {
|
|
|
74
191
|
}
|
|
75
192
|
|
|
76
193
|
/**
|
|
77
|
-
* 签名 Nostr 事件。
|
|
78
194
|
* @param {number} kind 事件 kind
|
|
79
195
|
* @param {string[][]} tags 事件标签
|
|
80
196
|
* @param {string} content 事件内容
|
|
@@ -91,7 +207,6 @@ async function signNostrEvent(kind, tags, content, secretKey) {
|
|
|
91
207
|
}
|
|
92
208
|
|
|
93
209
|
/**
|
|
94
|
-
* 连接 Nostr 中继 WebSocket(`ws` 包,支持 terminate)。
|
|
95
210
|
* @param {string} relayUrl 中继 URL
|
|
96
211
|
* @param {number} [timeoutMs] 超时毫秒
|
|
97
212
|
* @param {AbortSignal} [signal] 取消信号
|
|
@@ -138,13 +253,13 @@ function connectRelay(relayUrl, timeoutMs = NOSTR_CONNECT_TIMEOUT_MS, signal) {
|
|
|
138
253
|
}
|
|
139
254
|
|
|
140
255
|
/**
|
|
141
|
-
* 向多个中继并行发布 Nostr 事件(任一成功即可)。
|
|
142
256
|
* @param {string[]} relayUrls 中继 URL 列表
|
|
143
257
|
* @param {object} event 待发布事件
|
|
144
258
|
* @param {AbortSignal} [signal] 取消信号
|
|
145
259
|
* @returns {Promise<void>}
|
|
146
260
|
*/
|
|
147
261
|
async function publishEvent(relayUrls, event, signal) {
|
|
262
|
+
if (!relayUrls.length) throw new Error('nostr: no relay')
|
|
148
263
|
let published = false
|
|
149
264
|
let lastError = null
|
|
150
265
|
await Promise.allSettled(relayUrls.map(async relayUrl => {
|
|
@@ -167,7 +282,6 @@ async function publishEvent(relayUrls, event, signal) {
|
|
|
167
282
|
}
|
|
168
283
|
|
|
169
284
|
/**
|
|
170
|
-
* 并行渐进连接中继:立刻返回,连上后回调;abort 后 stop 接入并 drop 已开 socket。
|
|
171
285
|
* @param {string[]} relayUrls 中继列表
|
|
172
286
|
* @param {(ws: import('ws').WebSocket, relayUrl: string) => void} onOpen 连上回调
|
|
173
287
|
* @param {AbortSignal} signal 取消信号
|
|
@@ -182,18 +296,24 @@ function connectRelaysProgressive(relayUrls, onOpen, signal, sockets) {
|
|
|
182
296
|
return
|
|
183
297
|
}
|
|
184
298
|
sockets.push(ws)
|
|
299
|
+
nodeDebug('p2p:nostr relay up', { url: relayUrl })
|
|
185
300
|
onOpen(ws, relayUrl)
|
|
186
|
-
}).catch(
|
|
301
|
+
}).catch(error => {
|
|
302
|
+
nodeDebug('p2p:nostr relay fail', {
|
|
303
|
+
url: relayUrl,
|
|
304
|
+
err: String(error?.message || error),
|
|
305
|
+
})
|
|
306
|
+
})
|
|
187
307
|
}
|
|
188
308
|
|
|
189
309
|
/**
|
|
190
|
-
*
|
|
310
|
+
* 内部:按 rendezvous 键订阅 Nostr kind(topic 不导出)。
|
|
191
311
|
* @param {string[]} relayUrls 中继 URL 列表
|
|
192
|
-
* @param {{ kind: number,
|
|
312
|
+
* @param {{ kind: number, rendezvousKey: string, tagX: string, onPayload: (bytes: Uint8Array, meta: { relayUrl: string, event: object }) => void | Promise<void> }} options 订阅选项
|
|
193
313
|
* @returns {() => void} 取消订阅
|
|
194
314
|
*/
|
|
195
315
|
function subscribeNostrKind(relayUrls, options) {
|
|
196
|
-
const { kind,
|
|
316
|
+
const { kind, rendezvousKey, tagX, onPayload } = options
|
|
197
317
|
const abortController = new AbortController()
|
|
198
318
|
/** @type {import('ws').WebSocket[]} */
|
|
199
319
|
const sockets = []
|
|
@@ -206,10 +326,13 @@ function subscribeNostrKind(relayUrls, options) {
|
|
|
206
326
|
if (parsed?.[0] !== 'EVENT') return
|
|
207
327
|
const nostrEvent = parsed[2]
|
|
208
328
|
if (nostrEvent?.kind !== kind) return
|
|
209
|
-
try {
|
|
329
|
+
try {
|
|
330
|
+
const result = onPayload(base64ToBytes(nostrEvent.content), { relayUrl, event: nostrEvent })
|
|
331
|
+
if (result?.then) void result.catch(() => { })
|
|
332
|
+
}
|
|
210
333
|
catch { /* ignore */ }
|
|
211
334
|
})
|
|
212
|
-
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [kind], '#t': [
|
|
335
|
+
ws.send(JSON.stringify(['REQ', subscriptionId, { kinds: [kind], '#t': [rendezvousKey], '#x': [tagX] }]))
|
|
213
336
|
}, abortController.signal, sockets)
|
|
214
337
|
return () => {
|
|
215
338
|
abortController.abort()
|
|
@@ -218,92 +341,304 @@ function subscribeNostrKind(relayUrls, options) {
|
|
|
218
341
|
}
|
|
219
342
|
|
|
220
343
|
/**
|
|
221
|
-
* 创建 Nostr discovery provider
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* 省略 / `undefined` → 默认公网中继;显式 `[]` 表示无中继(不再回填默认)。
|
|
225
|
-
* @param {{ relayUrls?: string[] | null }} [options] 可选最终中继 URL 列表
|
|
226
|
-
* @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
|
|
344
|
+
* 创建 Nostr discovery provider(list+connect;topic 仅内部)。
|
|
345
|
+
* @param {{ relayUrls?: string[] | null, getRelayUrls?: () => string[] | null | undefined }} [options] 中继配置
|
|
346
|
+
* @returns {import('./index.mjs').DiscoveryProvider} Nostr discovery provider
|
|
227
347
|
*/
|
|
228
348
|
export function createNostrDiscoveryProvider(options = {}) {
|
|
229
|
-
|
|
349
|
+
/**
|
|
350
|
+
* @returns {string[]} 去重后的中继 URL 列表
|
|
351
|
+
*/
|
|
352
|
+
const resolveRelayUrls = () => {
|
|
353
|
+
if (typeof options.getRelayUrls === 'function') {
|
|
354
|
+
const urls = options.getRelayUrls()
|
|
355
|
+
return dedupeRelayUrls(urls == null ? DEFAULT_RELAY_URLS : urls)
|
|
356
|
+
}
|
|
357
|
+
if (options.relayUrls === undefined || options.relayUrls === null)
|
|
358
|
+
return dedupeRelayUrls(DEFAULT_RELAY_URLS)
|
|
359
|
+
return dedupeRelayUrls(options.relayUrls)
|
|
360
|
+
}
|
|
230
361
|
const secretKey = randomBytes(32)
|
|
362
|
+
/** @type {(() => void) | null} */
|
|
363
|
+
let stopNetworkAdvertSub = null
|
|
364
|
+
/** @type {Map<string, () => void>} */
|
|
365
|
+
const groupSubs = new Map()
|
|
366
|
+
/** @type {Map<string, () => void>} */
|
|
367
|
+
const nodeAdvertSubs = new Map()
|
|
368
|
+
/** @type {Map<string, () => void>} */
|
|
369
|
+
const nodeSignalSubs = new Map()
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @returns {void}
|
|
373
|
+
*/
|
|
374
|
+
function ensureNetworkAdvertSubscription() {
|
|
375
|
+
if (stopNetworkAdvertSub) return
|
|
376
|
+
const rendezvousKey = networkRendezvousKey()
|
|
377
|
+
stopNetworkAdvertSub = subscribeNostrKind(resolveRelayUrls(), {
|
|
378
|
+
kind: NOSTR_ADVERT_KIND,
|
|
379
|
+
rendezvousKey,
|
|
380
|
+
tagX: 'advert',
|
|
381
|
+
/**
|
|
382
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
383
|
+
* @returns {Promise<void>}
|
|
384
|
+
*/
|
|
385
|
+
async onPayload(bytes) {
|
|
386
|
+
await acceptNostrAdvert(rendezvousKey, bytes)
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @param {string} roomSecret 房间密钥
|
|
393
|
+
* @returns {void}
|
|
394
|
+
*/
|
|
395
|
+
function ensureGroupSubscription(roomSecret) {
|
|
396
|
+
const key = String(roomSecret || '')
|
|
397
|
+
if (!key || groupSubs.has(key)) return
|
|
398
|
+
const rendezvousKey = groupRendezvousKey(key)
|
|
399
|
+
groupSubs.set(key, subscribeNostrKind(resolveRelayUrls(), {
|
|
400
|
+
kind: NOSTR_ADVERT_KIND,
|
|
401
|
+
rendezvousKey,
|
|
402
|
+
tagX: 'advert',
|
|
403
|
+
/**
|
|
404
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
async onPayload(bytes) {
|
|
408
|
+
await acceptNostrAdvert(rendezvousKey, bytes, { roomSecret: key })
|
|
409
|
+
},
|
|
410
|
+
}))
|
|
411
|
+
}
|
|
412
|
+
|
|
231
413
|
return {
|
|
232
414
|
id: 'nostr',
|
|
233
415
|
priority: 100,
|
|
234
416
|
caps: { canDiscover: true, canSignal: true, canRelay: false },
|
|
235
417
|
/**
|
|
236
|
-
*
|
|
237
|
-
* @
|
|
238
|
-
|
|
239
|
-
|
|
418
|
+
* @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
|
|
419
|
+
* @returns {Promise<string[]>} 可见 nodeHash;带 roomSecret 时仅返回该群池
|
|
420
|
+
*/
|
|
421
|
+
async listVisibleNodeHashes(options = {}) {
|
|
422
|
+
const limit = Math.max(1, Number(options.limit) || 64)
|
|
423
|
+
if (options.roomSecret) {
|
|
424
|
+
ensureGroupSubscription(options.roomSecret)
|
|
425
|
+
return listNostrGroupVisibleNodeHashes(options.roomSecret).slice(0, limit)
|
|
426
|
+
}
|
|
427
|
+
ensureNetworkAdvertSubscription()
|
|
428
|
+
return listNostrVisibleNodeHashes().slice(0, limit)
|
|
429
|
+
},
|
|
430
|
+
/**
|
|
431
|
+
* 挂上对该节点的内部 advert 订阅(建链由 registry dialer / ensureLinkToNode 完成)。
|
|
432
|
+
* @param {string} nodeHash 目标
|
|
433
|
+
* @returns {Promise<boolean>} 是否已准备
|
|
240
434
|
*/
|
|
241
|
-
async
|
|
435
|
+
async connectToNode(nodeHash) {
|
|
436
|
+
const hash = normalizeHex64(nodeHash)
|
|
437
|
+
if (!isHex64(hash)) return false
|
|
438
|
+
if (!nodeAdvertSubs.has(hash)) {
|
|
439
|
+
const rendezvousKey = nodeRendezvousKey(hash)
|
|
440
|
+
nodeAdvertSubs.set(hash, subscribeNostrKind(resolveRelayUrls(), {
|
|
441
|
+
kind: NOSTR_ADVERT_KIND,
|
|
442
|
+
rendezvousKey,
|
|
443
|
+
tagX: 'advert',
|
|
444
|
+
/**
|
|
445
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
446
|
+
* @returns {Promise<void>}
|
|
447
|
+
*/
|
|
448
|
+
async onPayload(bytes) {
|
|
449
|
+
await acceptNostrAdvert(rendezvousKey, bytes)
|
|
450
|
+
},
|
|
451
|
+
}))
|
|
452
|
+
}
|
|
453
|
+
return true
|
|
454
|
+
},
|
|
455
|
+
/**
|
|
456
|
+
* @param {() => Promise<object | null>} getBeacon 本机 advert body 工厂
|
|
457
|
+
* @returns {Promise<() => void>} 停止函数
|
|
458
|
+
*/
|
|
459
|
+
async startPresence(getBeacon) {
|
|
460
|
+
const rendezvousKey = networkRendezvousKey()
|
|
242
461
|
const abortController = new AbortController()
|
|
462
|
+
ensureNetworkAdvertSubscription()
|
|
243
463
|
/**
|
|
244
|
-
* 向中继发布当前 advert。
|
|
245
464
|
* @returns {Promise<void>}
|
|
246
465
|
*/
|
|
247
466
|
const publish = async () => {
|
|
248
467
|
if (abortController.signal.aborted) return
|
|
468
|
+
const beacon = await getBeacon?.()
|
|
469
|
+
if (!beacon?.nodeHash) return
|
|
470
|
+
noteNostrVisibleNode(beacon.nodeHash)
|
|
471
|
+
const advertBody = beacon.advertBody || beacon.body || beacon
|
|
472
|
+
const bytes = encryptSignalPacket(rendezvousKey, { type: 'advert', body: advertBody })
|
|
249
473
|
const event = await signNostrEvent(
|
|
250
474
|
NOSTR_ADVERT_KIND,
|
|
251
|
-
[['t',
|
|
475
|
+
[['t', rendezvousKey], ['x', 'advert']],
|
|
252
476
|
bytesToBase64(bytes),
|
|
253
477
|
secretKey,
|
|
254
478
|
)
|
|
255
|
-
await publishEvent(
|
|
479
|
+
await publishEvent(resolveRelayUrls(), event, abortController.signal)
|
|
480
|
+
nodeDebug('p2p:nostr presence published', { self: shortHash(beacon.nodeHash) })
|
|
256
481
|
}
|
|
257
|
-
void publish().catch(
|
|
258
|
-
|
|
482
|
+
void publish().catch(error => {
|
|
483
|
+
nodeDebug('p2p:nostr presence publish fail', { err: String(error?.message || error) })
|
|
484
|
+
})
|
|
485
|
+
const timer = setInterval(() => {
|
|
486
|
+
void publish().catch(error => {
|
|
487
|
+
nodeDebug('p2p:nostr presence publish fail', { err: String(error?.message || error) })
|
|
488
|
+
})
|
|
489
|
+
}, 5 * 60_000)
|
|
490
|
+
timer.unref?.()
|
|
259
491
|
return () => {
|
|
260
492
|
abortController.abort()
|
|
261
493
|
clearInterval(timer)
|
|
262
494
|
}
|
|
263
495
|
},
|
|
264
496
|
/**
|
|
265
|
-
*
|
|
266
|
-
* @param {
|
|
267
|
-
* @param {Function} onAdvert advert 回调
|
|
268
|
-
* @returns {Promise<() => void>} 取消订阅函数
|
|
269
|
-
*/
|
|
270
|
-
async subscribe(topic, onAdvert) {
|
|
271
|
-
return subscribeNostrKind(relayUrls, {
|
|
272
|
-
kind: NOSTR_ADVERT_KIND,
|
|
273
|
-
topic,
|
|
274
|
-
tagX: 'advert',
|
|
275
|
-
onPayload: onAdvert,
|
|
276
|
-
})
|
|
277
|
-
},
|
|
278
|
-
/**
|
|
279
|
-
* 向中继发布 signal 事件(按需发送,仍等待至少一路成功)。
|
|
280
|
-
* @param {string} topic 信令 topic
|
|
281
|
-
* @param {string} to 目标节点标识
|
|
282
|
-
* @param {Uint8Array} bytes 信令载荷
|
|
497
|
+
* @param {string} toNodeHash 目标 nodeHash
|
|
498
|
+
* @param {Uint8Array} bytes 加密信令
|
|
283
499
|
* @returns {Promise<void>}
|
|
284
500
|
*/
|
|
285
|
-
async
|
|
501
|
+
async sendNodeSignal(toNodeHash, bytes) {
|
|
502
|
+
const hash = normalizeHex64(toNodeHash)
|
|
503
|
+
if (!isHex64(hash)) throw new Error('nostr: invalid nodeHash')
|
|
504
|
+
const rendezvousKey = nodeRendezvousKey(hash)
|
|
286
505
|
const event = await signNostrEvent(
|
|
287
506
|
NOSTR_SIGNAL_KIND,
|
|
288
|
-
[['t',
|
|
507
|
+
[['t', rendezvousKey], ['x', 'signal'], ['p', hash]],
|
|
289
508
|
bytesToBase64(bytes),
|
|
290
509
|
secretKey,
|
|
291
510
|
)
|
|
292
|
-
await publishEvent(
|
|
511
|
+
await publishEvent(resolveRelayUrls(), event)
|
|
293
512
|
},
|
|
294
513
|
/**
|
|
295
|
-
*
|
|
296
|
-
* @param {
|
|
297
|
-
* @
|
|
298
|
-
* @returns {Promise<() => void>} 取消订阅函数
|
|
514
|
+
* @param {string} localNodeHash 本机 nodeHash
|
|
515
|
+
* @param {(bytes: Uint8Array) => void} onSignal 信令回调
|
|
516
|
+
* @returns {Promise<() => void>} 取消函数
|
|
299
517
|
*/
|
|
300
|
-
async
|
|
301
|
-
|
|
518
|
+
async listenNodeSignals(localNodeHash, onSignal) {
|
|
519
|
+
const hash = normalizeHex64(localNodeHash)
|
|
520
|
+
if (!isHex64(hash)) throw new Error('nostr: invalid nodeHash')
|
|
521
|
+
const rendezvousKey = nodeRendezvousKey(hash)
|
|
522
|
+
const existing = nodeSignalSubs.get(hash)
|
|
523
|
+
if (existing) existing()
|
|
524
|
+
nodeDebug('p2p:nostr signal listen', { self: shortHash(hash), relays: resolveRelayUrls().length })
|
|
525
|
+
const stop = subscribeNostrKind(resolveRelayUrls(), {
|
|
302
526
|
kind: NOSTR_SIGNAL_KIND,
|
|
303
|
-
|
|
527
|
+
rendezvousKey,
|
|
304
528
|
tagX: 'signal',
|
|
305
529
|
onPayload: onSignal,
|
|
306
530
|
})
|
|
531
|
+
nodeSignalSubs.set(hash, stop)
|
|
532
|
+
return () => {
|
|
533
|
+
stop()
|
|
534
|
+
nodeSignalSubs.delete(hash)
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
/**
|
|
538
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
539
|
+
* @param {(bytes: Uint8Array, meta: object) => void} onAdvert advert 回调
|
|
540
|
+
* @returns {Promise<() => void>} 取消函数
|
|
541
|
+
*/
|
|
542
|
+
async watchNodeAdvert(nodeHash, onAdvert) {
|
|
543
|
+
const hash = normalizeHex64(nodeHash)
|
|
544
|
+
if (!isHex64(hash)) throw new Error('nostr: invalid nodeHash')
|
|
545
|
+
const rendezvousKey = nodeRendezvousKey(hash)
|
|
546
|
+
const stop = subscribeNostrKind(resolveRelayUrls(), {
|
|
547
|
+
kind: NOSTR_ADVERT_KIND,
|
|
548
|
+
rendezvousKey,
|
|
549
|
+
tagX: 'advert',
|
|
550
|
+
/**
|
|
551
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
552
|
+
* @param {object} meta relay 元数据
|
|
553
|
+
* @returns {void}
|
|
554
|
+
*/
|
|
555
|
+
onPayload: (bytes, meta) => onAdvert(bytes, meta),
|
|
556
|
+
})
|
|
557
|
+
return stop
|
|
558
|
+
},
|
|
559
|
+
/**
|
|
560
|
+
* @param {string} roomSecret 房间密钥
|
|
561
|
+
* @param {() => Promise<object | null>} getBeacon advert 工厂
|
|
562
|
+
* @returns {Promise<() => void>} 停止群 presence 广播
|
|
563
|
+
*/
|
|
564
|
+
async startGroupPresence(roomSecret, getBeacon) {
|
|
565
|
+
const key = String(roomSecret || '')
|
|
566
|
+
const rendezvousKey = groupRendezvousKey(key)
|
|
567
|
+
const abortController = new AbortController()
|
|
568
|
+
ensureGroupSubscription(key)
|
|
569
|
+
/**
|
|
570
|
+
* @returns {Promise<void>}
|
|
571
|
+
*/
|
|
572
|
+
const publish = async () => {
|
|
573
|
+
if (abortController.signal.aborted) return
|
|
574
|
+
const beacon = await getBeacon?.()
|
|
575
|
+
if (!beacon?.nodeHash) return
|
|
576
|
+
noteNostrGroupVisibleNode(key, beacon.nodeHash)
|
|
577
|
+
const advertBody = beacon.advertBody || beacon.body || beacon
|
|
578
|
+
const bytes = encryptSignalPacket(rendezvousKey, { type: 'advert', body: advertBody })
|
|
579
|
+
const event = await signNostrEvent(
|
|
580
|
+
NOSTR_ADVERT_KIND,
|
|
581
|
+
[['t', rendezvousKey], ['x', 'advert']],
|
|
582
|
+
bytesToBase64(bytes),
|
|
583
|
+
secretKey,
|
|
584
|
+
)
|
|
585
|
+
await publishEvent(resolveRelayUrls(), event, abortController.signal)
|
|
586
|
+
}
|
|
587
|
+
void publish().catch(() => { })
|
|
588
|
+
const timer = setInterval(() => { void publish().catch(() => { }) }, 5 * 60_000)
|
|
589
|
+
timer.unref?.()
|
|
590
|
+
return () => {
|
|
591
|
+
abortController.abort()
|
|
592
|
+
clearInterval(timer)
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
/**
|
|
596
|
+
* @param {string} roomSecret 房间密钥
|
|
597
|
+
* @param {(bytes: Uint8Array, meta: object) => void} onAdvert 回调
|
|
598
|
+
* @returns {Promise<() => void>} 取消群 advert 监听
|
|
599
|
+
*/
|
|
600
|
+
async watchGroupAdverts(roomSecret, onAdvert) {
|
|
601
|
+
const key = String(roomSecret || '')
|
|
602
|
+
const rendezvousKey = groupRendezvousKey(key)
|
|
603
|
+
ensureGroupSubscription(key)
|
|
604
|
+
return subscribeNostrKind(resolveRelayUrls(), {
|
|
605
|
+
kind: NOSTR_ADVERT_KIND,
|
|
606
|
+
rendezvousKey,
|
|
607
|
+
tagX: 'advert',
|
|
608
|
+
/**
|
|
609
|
+
* @param {Uint8Array} bytes 加密 advert 载荷
|
|
610
|
+
* @param {object} meta relay 元数据
|
|
611
|
+
* @returns {void}
|
|
612
|
+
*/
|
|
613
|
+
onPayload: (bytes, meta) => onAdvert(bytes, meta),
|
|
614
|
+
})
|
|
615
|
+
},
|
|
616
|
+
/**
|
|
617
|
+
* 供 advert 解析路径写入可见 hash。
|
|
618
|
+
* @param {string} nodeHash 节点 hash
|
|
619
|
+
* @param {{ roomSecret?: string }} [options] 带 roomSecret 时写入群池
|
|
620
|
+
* @returns {void}
|
|
621
|
+
*/
|
|
622
|
+
noteVisibleNode(nodeHash, options = {}) {
|
|
623
|
+
if (options.roomSecret) noteNostrGroupVisibleNode(options.roomSecret, nodeHash)
|
|
624
|
+
else noteNostrVisibleNode(nodeHash)
|
|
625
|
+
},
|
|
626
|
+
/**
|
|
627
|
+
* 停止全部内部订阅(reload / unregister 时调用)。
|
|
628
|
+
* @returns {void}
|
|
629
|
+
*/
|
|
630
|
+
dispose() {
|
|
631
|
+
stopNetworkAdvertSub?.()
|
|
632
|
+
stopNetworkAdvertSub = null
|
|
633
|
+
for (const stop of groupSubs.values())
|
|
634
|
+
try { stop() } catch { /* ignore */ }
|
|
635
|
+
groupSubs.clear()
|
|
636
|
+
for (const stop of nodeAdvertSubs.values())
|
|
637
|
+
try { stop() } catch { /* ignore */ }
|
|
638
|
+
nodeAdvertSubs.clear()
|
|
639
|
+
for (const stop of nodeSignalSubs.values())
|
|
640
|
+
try { stop() } catch { /* ignore */ }
|
|
641
|
+
nodeSignalSubs.clear()
|
|
307
642
|
},
|
|
308
643
|
}
|
|
309
644
|
}
|
|
@@ -4,9 +4,7 @@ import { verifiedChunkBytes } from '../files/chunk_fetch_verify.mjs'
|
|
|
4
4
|
/** @type {Map<string, { expectedHash: string, timer: ReturnType<typeof setTimeout>, resolve: (v: Uint8Array | null) => void, reject?: (e: Error) => void }>} */
|
|
5
5
|
export const pendingChunkFetches = new Map()
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
7
|
+
/** 并发 pending chunk fetch 上限。 */
|
|
10
8
|
export const MAX_PENDING_CHUNK_FETCHES = 2048
|
|
11
9
|
|
|
12
10
|
/**
|
|
@@ -20,10 +18,11 @@ export const MAX_PENDING_CHUNK_FETCHES = 2048
|
|
|
20
18
|
export function registerChunkFetchWait(key, expectedHash, timeoutMs, options = {}) {
|
|
21
19
|
if (!key || pendingChunkFetches.size >= MAX_PENDING_CHUNK_FETCHES)
|
|
22
20
|
return {
|
|
23
|
-
done: Promise.resolve(null),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
done: Promise.resolve(null),
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
cancel: () => { },
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
/** @type {(value: Uint8Array | null | Error) => void} */
|
|
@@ -58,11 +57,11 @@ export function registerChunkFetchWait(key, expectedHash, timeoutMs, options = {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
/**
|
|
61
|
-
* @param {Error | unknown}
|
|
60
|
+
* @param {Error | unknown} error 拒绝原因
|
|
62
61
|
* @returns {void}
|
|
63
62
|
*/
|
|
64
|
-
function rejectWait(
|
|
65
|
-
finish(
|
|
63
|
+
function rejectWait(error) {
|
|
64
|
+
finish(error instanceof Error ? error : new Error(String(error)))
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
pendingChunkFetches.set(key, {
|
|
@@ -74,9 +73,7 @@ export function registerChunkFetchWait(key, expectedHash, timeoutMs, options = {
|
|
|
74
73
|
|
|
75
74
|
return {
|
|
76
75
|
done,
|
|
77
|
-
/**
|
|
78
|
-
*
|
|
79
|
-
*/
|
|
76
|
+
/** 取消等待并以 null settle。 */
|
|
80
77
|
cancel: () => {
|
|
81
78
|
clearTimeout(timer)
|
|
82
79
|
pendingChunkFetches.delete(key)
|
|
@@ -3,9 +3,7 @@ import { verifySignedPublicManifest } from '../files/public_manifest.mjs'
|
|
|
3
3
|
/** @type {Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, resolve: (v: object | null) => void }>} */
|
|
4
4
|
export const pendingManifestFetches = new Map()
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
6
|
+
/** 并发 pending manifest fetch 上限。 */
|
|
9
7
|
export const MAX_PENDING_MANIFEST_FETCHES = 512
|
|
10
8
|
|
|
11
9
|
/**
|