@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/index.mjs
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
3
|
+
|
|
4
|
+
import { ingestGroupAdvert, ingestNodeAdvert } from './adverts.mjs'
|
|
5
|
+
import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousKey } from './internal/signal_crypto.mjs'
|
|
6
|
+
|
|
7
|
+
/** @typedef {{ id: string, priority: number, caps?: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, listVisibleNodeHashes?: (options?: { limit?: number, roomSecret?: string }) => Promise<string[]>, connectToNode?: (nodeHash: string, options?: object) => Promise<boolean | null>, startPresence?: (getBeacon: () => Promise<object | null>) => Promise<() => void>, startGroupPresence?: (roomSecret: string, getBeacon: () => Promise<object | null>) => Promise<() => void>, sendNodeSignal?: (toNodeHash: string, bytes: Uint8Array) => Promise<boolean | void>, listenNodeSignals?: (localNodeHash: string, onSignal: (bytes: Uint8Array) => void) => Promise<() => void>, watchNodeAdvert?: (nodeHash: string, onAdvert: (bytes: Uint8Array, meta: object) => void) => Promise<() => void>, watchGroupAdverts?: (roomSecret: string, onAdvert: (bytes: Uint8Array, meta: object) => void) => Promise<() => void>, noteVisibleNode?: (nodeHash: string, options?: { roomSecret?: string }) => void, dispose?: () => void }} DiscoveryProvider */
|
|
2
8
|
|
|
3
9
|
/** @type {Map<string, DiscoveryProvider>} */
|
|
4
10
|
const providers = new Map()
|
|
5
11
|
|
|
12
|
+
/** @type {((nodeHash: string) => Promise<object | null>) | null} */
|
|
13
|
+
let linkDialer = null
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 由 link registry 注入:discovery.connectToNode 经此建链(dialer 内负责 prepare)。
|
|
17
|
+
* @param {((nodeHash: string) => Promise<object | null>) | null} dialer 拨号函数
|
|
18
|
+
* @returns {void}
|
|
19
|
+
*/
|
|
20
|
+
export function setDiscoveryLinkDialer(dialer) {
|
|
21
|
+
linkDialer = dialer
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
/**
|
|
7
|
-
* 注册 discovery provider。
|
|
8
25
|
* @param {DiscoveryProvider} provider 发现提供者
|
|
9
26
|
* @returns {() => void} 注销函数
|
|
10
27
|
*/
|
|
@@ -15,111 +32,299 @@ export function registerDiscoveryProvider(provider) {
|
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
/**
|
|
18
|
-
* 注销 discovery provider。
|
|
19
35
|
* @param {string} id 提供者 id
|
|
20
36
|
* @returns {void}
|
|
21
37
|
*/
|
|
22
38
|
export function unregisterDiscoveryProvider(id) {
|
|
39
|
+
const provider = providers.get(String(id))
|
|
23
40
|
providers.delete(String(id))
|
|
41
|
+
try { provider?.dispose?.() } catch { /* ignore */ }
|
|
24
42
|
}
|
|
25
43
|
|
|
26
|
-
/**
|
|
27
|
-
* 清空全部 discovery provider。
|
|
28
|
-
* @returns {void}
|
|
29
|
-
*/
|
|
44
|
+
/** @returns {void} */
|
|
30
45
|
export function clearDiscoveryProviders() {
|
|
46
|
+
const list = [...providers.values()]
|
|
31
47
|
providers.clear()
|
|
48
|
+
for (const provider of list)
|
|
49
|
+
try { provider.dispose?.() } catch { /* ignore */ }
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
/**
|
|
35
|
-
* 列出已注册的 discovery provider(按 priority 排序)。
|
|
36
|
-
* @returns {DiscoveryProvider[]} 提供者列表
|
|
37
|
-
*/
|
|
52
|
+
/** @returns {DiscoveryProvider[]} 按 priority 升序排列的已注册提供者 */
|
|
38
53
|
export function listDiscoveryProviders() {
|
|
39
54
|
return [...providers.values()].sort((left, right) => Number(left.priority || 0) - Number(right.priority || 0))
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
* @param {string} id 提供者 id
|
|
59
|
+
* @returns {DiscoveryProvider | undefined} 对应 id 的提供者,未注册为 undefined
|
|
60
|
+
*/
|
|
61
|
+
export function getDiscoveryProvider(id) {
|
|
62
|
+
return providers.get(String(id))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} method provider 方法名
|
|
67
|
+
* @returns {DiscoveryProvider[]} 实现了指定方法的提供者列表
|
|
68
|
+
*/
|
|
69
|
+
function providersWith(method) {
|
|
70
|
+
return listDiscoveryProviders().filter(provider => typeof provider[method] === 'function')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {Array<(() => void) | null | undefined>} cleanups 清理函数
|
|
75
|
+
* @returns {() => void} 调用时依次执行各 cleanup
|
|
47
76
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
function composeCleanups(cleanups) {
|
|
78
|
+
return () => {
|
|
79
|
+
for (const stop of cleanups)
|
|
80
|
+
if (typeof stop === 'function') try { stop() } catch { /* ignore */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 对各实现了 method 的 provider 并行调用,合并 cleanup。
|
|
86
|
+
* @param {string} method provider 方法名
|
|
87
|
+
* @param {unknown[]} methodArguments 参数
|
|
88
|
+
* @param {{ errorLabel?: string, requireAny?: boolean }} [options] requireAny 默认 true
|
|
89
|
+
* @returns {Promise<() => void>} 合并各 provider 返回的 cleanup
|
|
90
|
+
*/
|
|
91
|
+
async function fanInProviderMethod(method, methodArguments, options = {}) {
|
|
92
|
+
const requireAny = options.requireAny !== false
|
|
93
|
+
const capable = providersWith(method)
|
|
94
|
+
if (requireAny && !capable.length) throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
|
|
95
|
+
const cleanups = await Promise.all(capable.map(async provider => {
|
|
50
96
|
try {
|
|
51
|
-
|
|
52
|
-
return typeof cleanup === 'function' ? cleanup : null
|
|
97
|
+
return await provider[method](...methodArguments)
|
|
53
98
|
}
|
|
54
99
|
catch {
|
|
55
100
|
return null
|
|
56
101
|
}
|
|
57
102
|
}))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
if (requireAny && !cleanups.some(stop => typeof stop === 'function'))
|
|
104
|
+
throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
|
|
105
|
+
return composeCleanups(cleanups)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 合并各 provider 可见 nodeHash(带 roomSecret 时各介质只返回本群池;不懂群的介质应返回 [])。
|
|
110
|
+
* @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
|
|
111
|
+
* @returns {Promise<string[]>} 去重后的 nodeHash 列表
|
|
112
|
+
*/
|
|
113
|
+
export async function listVisibleNodeHashes(options = {}) {
|
|
114
|
+
const limit = Math.max(1, Number(options.limit) || 64)
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
/** @type {Record<string, string[]>} */
|
|
117
|
+
const byProvider = {}
|
|
118
|
+
for (const provider of listDiscoveryProviders()) {
|
|
119
|
+
if (typeof provider.listVisibleNodeHashes !== 'function') continue
|
|
120
|
+
try {
|
|
121
|
+
const hashes = await provider.listVisibleNodeHashes(options)
|
|
122
|
+
byProvider[provider.id] = hashes.map(hash => shortHash(hash))
|
|
123
|
+
for (const hash of hashes) {
|
|
124
|
+
if (hash) seen.add(hash)
|
|
125
|
+
if (seen.size >= limit) break
|
|
126
|
+
}
|
|
127
|
+
if (seen.size >= limit) break
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
nodeDebug('p2p:discovery list fail', {
|
|
131
|
+
provider: provider.id,
|
|
132
|
+
err: String(error?.message || error),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
62
135
|
}
|
|
136
|
+
const out = [...seen].slice(0, limit)
|
|
137
|
+
nodeDebug('p2p:discovery visible', {
|
|
138
|
+
total: out.length,
|
|
139
|
+
group: !!options.roomSecret,
|
|
140
|
+
byProvider,
|
|
141
|
+
})
|
|
142
|
+
return out
|
|
63
143
|
}
|
|
64
144
|
|
|
65
145
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @
|
|
70
|
-
* @returns {Promise<() => void>} 统一取消函数
|
|
146
|
+
* 各发现介质准备通往 nodeHash 的路径(hint / 订阅 / 近场),不建链。
|
|
147
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
148
|
+
* @param {object} [options] 额外选项
|
|
149
|
+
* @returns {Promise<void>}
|
|
71
150
|
*/
|
|
72
|
-
export async function
|
|
73
|
-
const
|
|
74
|
-
|
|
151
|
+
export async function prepareConnectToNode(nodeHash, options = {}) {
|
|
152
|
+
const hash = normalizeHex64(nodeHash)
|
|
153
|
+
for (const provider of listDiscoveryProviders()) {
|
|
154
|
+
if (typeof provider.connectToNode !== 'function') continue
|
|
155
|
+
try { await provider.connectToNode(hash, options) }
|
|
156
|
+
catch { /* prepare next medium */ }
|
|
157
|
+
}
|
|
75
158
|
}
|
|
76
159
|
|
|
77
160
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* @param {
|
|
81
|
-
* @
|
|
82
|
-
* @returns {Promise<() => void>} 统一取消函数
|
|
161
|
+
* 经 registry dialer 建链(dialer 内 prepare);无 dialer 时仅 prepare,返回 false。
|
|
162
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
163
|
+
* @param {object} [options] 无 dialer 时转交 prepare
|
|
164
|
+
* @returns {Promise<boolean>} 是否建链成功
|
|
83
165
|
*/
|
|
84
|
-
export async function
|
|
85
|
-
const
|
|
86
|
-
|
|
166
|
+
export async function connectToNode(nodeHash, options = {}) {
|
|
167
|
+
const hash = normalizeHex64(nodeHash)
|
|
168
|
+
if (!linkDialer) {
|
|
169
|
+
await prepareConnectToNode(hash, options)
|
|
170
|
+
nodeDebug('p2p:discovery connect skip', { peer: shortHash(hash), reason: 'no-dialer' })
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const ok = !!await linkDialer(hash)
|
|
175
|
+
nodeDebug(ok ? 'p2p:discovery connect ok' : 'p2p:discovery connect miss', { peer: shortHash(hash) })
|
|
176
|
+
return ok
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
nodeDebug('p2p:discovery connect fail', {
|
|
180
|
+
peer: shortHash(hash),
|
|
181
|
+
err: String(error?.message || error),
|
|
182
|
+
})
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
87
185
|
}
|
|
88
186
|
|
|
89
187
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* @param {string} topic 信令 topic
|
|
94
|
-
* @param {string} to 目标节点标识
|
|
95
|
-
* @param {Uint8Array} bytes 信令载荷
|
|
188
|
+
* 经各可信令介质发送;任一成功即返回。
|
|
189
|
+
* @param {string} toNodeHash 目标 nodeHash
|
|
190
|
+
* @param {Uint8Array} bytes 载荷
|
|
96
191
|
* @returns {Promise<void>}
|
|
97
192
|
*/
|
|
98
|
-
export async function
|
|
99
|
-
const capable =
|
|
100
|
-
if (!capable.length) throw new Error('p2p:
|
|
101
|
-
let sent = false
|
|
193
|
+
export async function sendNodeSignal(toNodeHash, bytes) {
|
|
194
|
+
const capable = providersWith('sendNodeSignal')
|
|
195
|
+
if (!capable.length) throw new Error('p2p: signaling unavailable')
|
|
102
196
|
let lastError = null
|
|
103
|
-
|
|
197
|
+
for (const provider of capable)
|
|
104
198
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
const result = await provider.sendNodeSignal(toNodeHash, bytes)
|
|
200
|
+
if (result !== false) return
|
|
107
201
|
}
|
|
108
202
|
catch (error) {
|
|
109
203
|
lastError = error
|
|
110
204
|
}
|
|
111
|
-
|
|
112
|
-
if (
|
|
205
|
+
|
|
206
|
+
if (lastError) throw lastError
|
|
207
|
+
throw new Error('p2p: signaling unavailable')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 在各可信令介质上监听;返回统一取消。
|
|
212
|
+
* @param {string} localNodeHash 本机 nodeHash
|
|
213
|
+
* @param {(bytes: Uint8Array) => void} onSignal 回调
|
|
214
|
+
* @returns {Promise<() => void>} 取消函数
|
|
215
|
+
*/
|
|
216
|
+
export async function listenNodeSignals(localNodeHash, onSignal) {
|
|
217
|
+
return fanInProviderMethod('listenNodeSignals', [localNodeHash, onSignal], {
|
|
218
|
+
errorLabel: 'p2p: signaling unavailable',
|
|
219
|
+
})
|
|
113
220
|
}
|
|
114
221
|
|
|
115
222
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* @
|
|
119
|
-
* @param {(bytes: Uint8Array, meta?: object) => void} onSignal 信令回调
|
|
120
|
-
* @returns {Promise<() => void>} 统一取消函数
|
|
223
|
+
* 启动各 provider 的 presence(若支持)。
|
|
224
|
+
* @param {() => Promise<object | null>} getBeacon 本机 beacon
|
|
225
|
+
* @returns {Promise<() => void>} 统一停止函数
|
|
121
226
|
*/
|
|
122
|
-
export async function
|
|
123
|
-
|
|
124
|
-
return startProvidersParallel(list, provider => provider.onSignal(topic, onSignal))
|
|
227
|
+
export async function startDiscoveryPresence(getBeacon) {
|
|
228
|
+
return fanInProviderMethod('startPresence', [getBeacon], { requireAny: false })
|
|
125
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 监听指定 nodeHash 的 advert(各支持介质 fan-in)。
|
|
233
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
234
|
+
* @param {(bytes: Uint8Array, meta: object) => void} onAdvert 回调
|
|
235
|
+
* @returns {Promise<() => void>} 取消函数
|
|
236
|
+
*/
|
|
237
|
+
export async function watchNodeAdvert(nodeHash, onAdvert) {
|
|
238
|
+
return fanInProviderMethod('watchNodeAdvert', [nodeHash, onAdvert], {
|
|
239
|
+
errorLabel: 'p2p: node advert watch unavailable',
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 群 advert 监听(各支持介质 fan-in)。
|
|
245
|
+
* @param {string} roomSecret 房间密钥
|
|
246
|
+
* @param {(bytes: Uint8Array, meta: object) => void} onAdvert 回调
|
|
247
|
+
* @returns {Promise<() => void>} 取消群 advert 监听
|
|
248
|
+
*/
|
|
249
|
+
export async function watchGroupAdverts(roomSecret, onAdvert) {
|
|
250
|
+
return fanInProviderMethod('watchGroupAdverts', [roomSecret, onAdvert], {
|
|
251
|
+
errorLabel: 'p2p: group advert watch unavailable',
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 群 presence 广播(各支持介质 fan-out)。
|
|
257
|
+
* @param {string} roomSecret 房间密钥
|
|
258
|
+
* @param {() => Promise<object | null>} getBeacon advert 工厂
|
|
259
|
+
* @returns {Promise<() => void>} 停止群 presence 广播
|
|
260
|
+
*/
|
|
261
|
+
export async function startGroupPresence(roomSecret, getBeacon) {
|
|
262
|
+
return fanInProviderMethod('startGroupPresence', [roomSecret, getBeacon], {
|
|
263
|
+
errorLabel: 'p2p: group presence unavailable',
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 向节点发送 JSON 信令包(discovery 内部加解密)。
|
|
269
|
+
* @param {string} toNodeHash 目标 nodeHash
|
|
270
|
+
* @param {unknown} packet JSON 载荷
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
export async function sendNodeSignalPacket(toNodeHash, packet) {
|
|
274
|
+
const hash = normalizeHex64(toNodeHash)
|
|
275
|
+
await sendNodeSignal(hash, encryptSignalPacket(nodeRendezvousKey(hash), packet))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 解密发往本机的节点信令包。
|
|
280
|
+
* @param {string} localNodeHash 本机 nodeHash
|
|
281
|
+
* @param {Uint8Array} bytes 加密字节
|
|
282
|
+
* @returns {object | null} 解密 JSON
|
|
283
|
+
*/
|
|
284
|
+
export function decryptNodeSignalPacket(localNodeHash, bytes) {
|
|
285
|
+
return decryptSignalPacket(nodeRendezvousKey(normalizeHex64(localNodeHash)), bytes)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* advert 验签后写入各介质可见池。
|
|
290
|
+
* @param {string} verifiedNodeHash 已验签 nodeHash
|
|
291
|
+
* @param {{ roomSecret?: string }} [options] 带 roomSecret 时写入群池
|
|
292
|
+
* @returns {void}
|
|
293
|
+
*/
|
|
294
|
+
export function noteVisibleNodeFromAdvert(verifiedNodeHash, options = {}) {
|
|
295
|
+
for (const provider of listDiscoveryProviders())
|
|
296
|
+
provider.noteVisibleNode?.(verifiedNodeHash, options)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 监听指定 nodeHash 的 advert,验签后回调。
|
|
301
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
302
|
+
* @param {(verifiedNodeHash: string, body: object, meta: object) => void | Promise<void>} onAdvert 回调
|
|
303
|
+
* @returns {Promise<() => void>} 取消函数
|
|
304
|
+
*/
|
|
305
|
+
export async function watchVerifiedNodeAdvert(nodeHash, onAdvert) {
|
|
306
|
+
return await watchNodeAdvert(nodeHash, async (bytes, meta) => {
|
|
307
|
+
const ingested = await ingestNodeAdvert(nodeHash, bytes, meta)
|
|
308
|
+
if (!ingested) return
|
|
309
|
+
noteVisibleNodeFromAdvert(ingested.verifiedNodeHash)
|
|
310
|
+
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 群 advert 监听 + 验签;写入群可见池。
|
|
316
|
+
* @param {string} roomSecret 房间密钥
|
|
317
|
+
* @param {(verifiedNodeHash: string, body: object, meta: object) => void | Promise<void>} onAdvert 回调
|
|
318
|
+
* @returns {Promise<() => void>} 取消群 advert 验签监听
|
|
319
|
+
*/
|
|
320
|
+
export async function watchVerifiedGroupAdverts(roomSecret, onAdvert) {
|
|
321
|
+
return await watchGroupAdverts(roomSecret, async (bytes, meta) => {
|
|
322
|
+
const ingested = await ingestGroupAdvert(roomSecret, bytes, meta)
|
|
323
|
+
if (!ingested) return
|
|
324
|
+
noteVisibleNodeFromAdvert(ingested.verifiedNodeHash, { roomSecret })
|
|
325
|
+
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** 再导出:scope advert 构建/加密/验签(见 `adverts.mjs`)。 */
|
|
330
|
+
export { buildSignedAdvertForScope, encryptAdvertForScope, encryptAdvertPacket, ingestEncryptedAdvert, ingestGroupAdvert, ingestNetworkAdvert, ingestNodeAdvert } from './adverts.mjs'
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
5
|
+
import { sha256Hex } from '../../crypto/crypto.mjs'
|
|
6
|
+
import { createLruMap } from '../../utils/lru.mjs'
|
|
7
|
+
|
|
8
|
+
const SIGNAL_DOMAIN = 'fount-signal'
|
|
9
|
+
const NODE_RENDEZVOUS_DOMAIN = 'fount-rdv-node:'
|
|
10
|
+
const GROUP_RENDEZVOUS_DOMAIN = 'fount-rdv-group:'
|
|
11
|
+
const NETWORK_RENDEZVOUS_DOMAIN = 'fount-rdv-network:'
|
|
12
|
+
|
|
13
|
+
/** rendezvous 键 → AES-256 密钥缓存上限 */
|
|
14
|
+
export const SIGNAL_KEY_CACHE_MAX = 512
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 节点 rendezvous 键(discovery advert / signal)。
|
|
18
|
+
* @param {string} nodeHash 节点 64 hex
|
|
19
|
+
* @returns {string} rendezvous 键
|
|
20
|
+
*/
|
|
21
|
+
export function nodeRendezvousKey(nodeHash) {
|
|
22
|
+
return sha256Hex(`${NODE_RENDEZVOUS_DOMAIN}${normalizeHex64(nodeHash)}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 群组 rendezvous 键(roomSecret)。
|
|
27
|
+
* @param {string} roomSecret 房间密钥
|
|
28
|
+
* @returns {string} rendezvous 键
|
|
29
|
+
*/
|
|
30
|
+
export function groupRendezvousKey(roomSecret) {
|
|
31
|
+
return sha256Hex(`${GROUP_RENDEZVOUS_DOMAIN}${roomSecret}`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 全网 network-scope rendezvous 键。
|
|
36
|
+
* @returns {string} rendezvous 键
|
|
37
|
+
*/
|
|
38
|
+
export function networkRendezvousKey() {
|
|
39
|
+
return sha256Hex(NETWORK_RENDEZVOUS_DOMAIN)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** rendezvous 键 → AES-256 密钥 LRU */
|
|
43
|
+
const signalKeysByKey = createLruMap(SIGNAL_KEY_CACHE_MAX)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} rendezvousKey rendezvous 键
|
|
47
|
+
* @returns {Buffer} AES-256 密钥
|
|
48
|
+
*/
|
|
49
|
+
function signalKeyForRendezvous(rendezvousKey) {
|
|
50
|
+
let cached = signalKeysByKey.get(rendezvousKey)
|
|
51
|
+
if (cached) {
|
|
52
|
+
signalKeysByKey.touch(rendezvousKey, cached)
|
|
53
|
+
return cached
|
|
54
|
+
}
|
|
55
|
+
cached = createHash('sha256').update(`${SIGNAL_DOMAIN}:${rendezvousKey}`).digest()
|
|
56
|
+
signalKeysByKey.touch(rendezvousKey, cached)
|
|
57
|
+
return cached
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @returns {number} 密钥缓存条目数(测试用) */
|
|
61
|
+
export function signalKeyCacheSize() {
|
|
62
|
+
return signalKeysByKey.size
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* AES-GCM 封装 JSON 信令 / advert 包。
|
|
67
|
+
* @param {string} rendezvousKey rendezvous 键
|
|
68
|
+
* @param {unknown} packet 待加密 JSON
|
|
69
|
+
* @returns {Uint8Array} 加密字节
|
|
70
|
+
*/
|
|
71
|
+
export function encryptSignalPacket(rendezvousKey, packet) {
|
|
72
|
+
const iv = randomBytes(12)
|
|
73
|
+
const cipher = createCipheriv('aes-256-gcm', signalKeyForRendezvous(rendezvousKey), iv)
|
|
74
|
+
const ciphertext = Buffer.concat([
|
|
75
|
+
cipher.update(Buffer.from(JSON.stringify(packet), 'utf8')),
|
|
76
|
+
cipher.final(),
|
|
77
|
+
])
|
|
78
|
+
return Buffer.from(JSON.stringify({
|
|
79
|
+
iv: iv.toString('base64'),
|
|
80
|
+
authTag: cipher.getAuthTag().toString('base64'),
|
|
81
|
+
ciphertext: ciphertext.toString('base64'),
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Untrusted ingress:AES-GCM 解密信令 / advert;失败返回 null。
|
|
87
|
+
* @param {string} rendezvousKey rendezvous 键
|
|
88
|
+
* @param {Uint8Array} bytes 加密字节
|
|
89
|
+
* @returns {object | null} 解密 JSON 或 null
|
|
90
|
+
*/
|
|
91
|
+
export function decryptSignalPacket(rendezvousKey, bytes) {
|
|
92
|
+
try {
|
|
93
|
+
const payload = JSON.parse(Buffer.from(bytes).toString('utf8'))
|
|
94
|
+
const decipher = createDecipheriv(
|
|
95
|
+
'aes-256-gcm',
|
|
96
|
+
signalKeyForRendezvous(rendezvousKey),
|
|
97
|
+
Buffer.from(payload.iv, 'base64'),
|
|
98
|
+
)
|
|
99
|
+
decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'))
|
|
100
|
+
const plain = Buffer.concat([
|
|
101
|
+
decipher.update(Buffer.from(payload.ciphertext, 'base64')),
|
|
102
|
+
decipher.final(),
|
|
103
|
+
])
|
|
104
|
+
return JSON.parse(plain.toString('utf8'))
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|