@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.
Files changed (75) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +1 -1
  7. package/discovery/adverts.mjs +109 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
@@ -1,10 +1,27 @@
1
- /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => boolean | void | Promise<boolean | void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
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
- * 并行启动各 provider async 钩子;单路失败静默。
44
- * @param {DiscoveryProvider[]} list 提供者列表
45
- * @param {(provider: DiscoveryProvider) => Promise<(() => void) | null | undefined>} run 单路启动
46
- * @returns {Promise<() => void>} 统一取消函数
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
- async function startProvidersParallel(list, run) {
49
- const settled = await Promise.all(list.map(async provider => {
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
- const cleanup = await run(provider)
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
- const cleanups = settled.filter(Boolean)
59
- return () => {
60
- for (const cleanup of cleanups)
61
- try { cleanup() } catch { /* ignore */ }
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
- * 通过所有可用 provider 广播 topic advert。
67
- * 单路失败静默(正常降级);其它 provider 仍可成功。
68
- * @param {string} topic advert 主题
69
- * @param {Uint8Array} bytes advert 载荷
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 advertiseTopic(topic, bytes) {
73
- const list = listDiscoveryProviders().filter(provider => provider.caps?.canDiscover && typeof provider.advertise === 'function')
74
- return startProvidersParallel(list, provider => provider.advertise(topic, bytes))
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
- * 通过所有可用 provider 订阅 topic advert
79
- * 单路失败静默(正常降级)。
80
- * @param {string} topic advert 主题
81
- * @param {(bytes: Uint8Array, meta?: object) => void} onAdvert advert 回调
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 subscribeTopic(topic, onAdvert) {
85
- const list = listDiscoveryProviders().filter(provider => provider.caps?.canDiscover && typeof provider.subscribe === 'function')
86
- return startProvidersParallel(list, provider => provider.subscribe(topic, onAdvert))
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
- * 通过 discovery provider 发送信令。
91
- * provider 返回 `false` = 本路不可达(正常降级,不计成功);其它返回值 / void = 已投递。
92
- * 单路 throw 静默;全部未投递才 throw。
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 sendSignal(topic, to, bytes) {
99
- const capable = listDiscoveryProviders().filter(provider => provider.caps?.canSignal && typeof provider.sendSignal === 'function')
100
- if (!capable.length) throw new Error('p2p: no discovery provider can signal')
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
- await Promise.all(capable.map(async provider => {
197
+ for (const provider of capable)
104
198
  try {
105
- if (await Promise.resolve(provider.sendSignal(topic, to, bytes)) !== false)
106
- sent = true
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 (!sent) throw lastError || new Error('p2p: no discovery provider delivered signal')
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
- * 通过所有可用 provider 监听信令。
117
- * 单路失败静默(正常降级)。
118
- * @param {string} topic 信令 topic
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 listenSignals(topic, onSignal) {
123
- const list = listDiscoveryProviders().filter(provider => provider.caps?.canSignal && typeof provider.onSignal === 'function')
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
+ }