@steve02081504/fount-p2p 0.0.0

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 (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,749 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
3
+
4
+ import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
5
+ import { sha256Hex, keyPairFromSeed } from '../crypto/crypto.mjs'
6
+ import { advertiseTopic, listenSignals, listDiscoveryProviders, registerDiscoveryProvider, sendSignal, subscribeTopic } from '../discovery/index.mjs'
7
+ import { createMdnsDiscoveryProvider } from '../discovery/mdns.mjs'
8
+ import { mergeSignalingRelayUrls, createNostrDiscoveryProvider } from '../discovery/nostr.mjs'
9
+ import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
10
+ import { createLink } from '../link/link.mjs'
11
+ import { ensureNodeSeed, getNodeHash, getNodeTransportSettings } from '../node/identity.mjs'
12
+ import { getSignalingRuntimeConfig } from '../node/instance.mjs'
13
+ import { createOverlayRouter } from '../overlay/index.mjs'
14
+ import { createLruMap } from '../utils/lru.mjs'
15
+
16
+ import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
17
+
18
+ const SIGNAL_DOMAIN = 'fount-signal-v1'
19
+ const NODE_TOPIC_DOMAIN = 'fount-rdv-node:'
20
+ const GROUP_TOPIC_DOMAIN = 'fount-rdv-group:'
21
+
22
+ /**
23
+ * 由 nodeHash 派生节点 rendezvous topic。
24
+ * @param {string} nodeHash 节点 64 hex
25
+ * @returns {string} rendezvous topic 哈希
26
+ */
27
+ export function nodeRendezvousTopic(nodeHash) {
28
+ return sha256Hex(`${NODE_TOPIC_DOMAIN}${normalizeHex64(nodeHash)}`)
29
+ }
30
+
31
+ /**
32
+ * 由房间密钥派生群组 rendezvous topic。
33
+ * @param {string} roomSecret 房间密钥
34
+ * @returns {string} rendezvous topic 哈希
35
+ */
36
+ export function groupRendezvousTopic(roomSecret) {
37
+ return sha256Hex(`${GROUP_TOPIC_DOMAIN}${String(roomSecret || '')}`)
38
+ }
39
+
40
+ /**
41
+ * 由 topic 派生信令 AES 密钥。
42
+ * @param {string} topic rendezvous 主题
43
+ * @returns {Buffer} AES-256 密钥
44
+ */
45
+ function signalKeyForTopic(topic) {
46
+ return createHash('sha256').update(`${SIGNAL_DOMAIN}:${String(topic)}`).digest()
47
+ }
48
+
49
+ /**
50
+ * 加密信令包为 AES-GCM 字节序列。
51
+ * @param {string} topic rendezvous 主题
52
+ * @param {unknown} packet 待加密 JSON 对象
53
+ * @returns {Uint8Array} 加密后的字节
54
+ */
55
+ export function encryptSignalPacket(topic, packet) {
56
+ const iv = randomBytes(12)
57
+ const cipher = createCipheriv('aes-256-gcm', signalKeyForTopic(topic), iv)
58
+ const ciphertext = Buffer.concat([
59
+ cipher.update(Buffer.from(JSON.stringify(packet), 'utf8')),
60
+ cipher.final(),
61
+ ])
62
+ return Buffer.from(JSON.stringify({
63
+ iv: iv.toString('base64'),
64
+ authTag: cipher.getAuthTag().toString('base64'),
65
+ ciphertext: ciphertext.toString('base64'),
66
+ }))
67
+ }
68
+
69
+ /**
70
+ * 解密信令包;失败时返回 null。
71
+ * @param {string} topic rendezvous 主题
72
+ * @param {Uint8Array} bytes 加密字节
73
+ * @returns {object | null} 解密后的 JSON 对象
74
+ */
75
+ export function decryptSignalPacket(topic, bytes) {
76
+ try {
77
+ const payload = JSON.parse(Buffer.from(bytes).toString('utf8'))
78
+ const decipher = createDecipheriv(
79
+ 'aes-256-gcm',
80
+ signalKeyForTopic(topic),
81
+ Buffer.from(payload.iv, 'base64'),
82
+ )
83
+ decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'))
84
+ const plain = Buffer.concat([
85
+ decipher.update(Buffer.from(payload.ciphertext, 'base64')),
86
+ decipher.final(),
87
+ ])
88
+ return JSON.parse(plain.toString('utf8'))
89
+ }
90
+ catch {
91
+ return null
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 解析或从节点种子推导本地身份。
97
+ * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array } | undefined} localIdentity 可选的预置身份
98
+ * @returns {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} 规范化后的本地身份
99
+ */
100
+ function resolveLocalIdentity(localIdentity) {
101
+ if (localIdentity?.nodeHash && localIdentity?.nodePubKey && localIdentity?.secretKey)
102
+ return {
103
+ nodeHash: normalizeHex64(localIdentity.nodeHash),
104
+ nodePubKey: normalizeHex64(localIdentity.nodePubKey),
105
+ secretKey: localIdentity.secretKey,
106
+ }
107
+ const secretKey = Buffer.from(ensureNodeSeed(), 'hex')
108
+ const { publicKey } = keyPairFromSeed(secretKey)
109
+ return {
110
+ nodeHash: getNodeHash(),
111
+ nodePubKey: Buffer.from(publicKey).toString('hex'),
112
+ secretKey,
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 创建带 backlog 的缓冲信令会话。
118
+ * @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
119
+ * @returns {{ send: (message: unknown) => Promise<void>, onRemote: (handler: (message: unknown) => void) => () => void, deliver: (message: unknown) => void, clear: () => void }} 信令会话
120
+ */
121
+ function createBufferedSignalSession(sendRemote) {
122
+ /** @type {Set<(message: unknown) => void>} */
123
+ const handlers = new Set()
124
+ /** @type {unknown[]} */
125
+ const backlog = []
126
+ return {
127
+ /**
128
+ * 发送信令消息到远端。
129
+ * @param {unknown} message 信令消息
130
+ * @returns {Promise<void>}
131
+ */
132
+ async send(message) {
133
+ await sendRemote(message)
134
+ },
135
+ /**
136
+ * 注册远端信令 handler(含 backlog 回放)。
137
+ * @param {(message: unknown) => void} handler 入站回调
138
+ * @returns {() => void} 取消订阅函数
139
+ */
140
+ onRemote(handler) {
141
+ handlers.add(handler)
142
+ for (const pending of backlog.splice(0))
143
+ handler(pending)
144
+ return () => handlers.delete(handler)
145
+ },
146
+ /**
147
+ * 投递信令消息;无 handler 时入 backlog。
148
+ * @param {unknown} message 信令消息
149
+ * @returns {void}
150
+ */
151
+ deliver(message) {
152
+ if (!handlers.size) {
153
+ backlog.push(message)
154
+ return
155
+ }
156
+ for (const handler of handlers)
157
+ handler(message)
158
+ },
159
+ /**
160
+ * 清空 backlog 与 handler。
161
+ * @returns {void}
162
+ */
163
+ clear() {
164
+ backlog.length = 0
165
+ handlers.clear()
166
+ },
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 向 Map<key, Set<listener>> 订阅并返回取消函数。
172
+ * @param {Map<string, Set<Function>>} buckets 监听器桶
173
+ * @param {string} key 桶键
174
+ * @param {Function} listener 监听器
175
+ * @returns {() => void} 取消订阅函数
176
+ */
177
+ function subscribeBucket(buckets, key, listener) {
178
+ if (!buckets.has(key)) buckets.set(key, new Set())
179
+ buckets.get(key).add(listener)
180
+ return () => {
181
+ const set = buckets.get(key)
182
+ if (!set) return
183
+ set.delete(listener)
184
+ if (!set.size) buckets.delete(key)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 创建 P2P 链路注册表(discovery、信令、直连与 overlay relay)。
190
+ * @param {object} [opts] 选项
191
+ * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [opts.localIdentity] 本地身份
192
+ * @param {typeof createLink} [opts.createLink] 链路工厂(可注入 mock)
193
+ * @param {RTCConfiguration['iceServers']} [opts.iceServers] ICE 服务器列表
194
+ * @param {number} [opts.maxActive] 最大并发活跃链路数
195
+ * @param {boolean} [opts.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
196
+ * @returns {object} link registry 接口
197
+ */
198
+ export function createLinkRegistry(opts = {}) {
199
+ const localIdentity = resolveLocalIdentity(opts.localIdentity)
200
+ const createLinkImpl = opts.createLink ?? createLink
201
+ const iceServers = opts.iceServers?.length ? opts.iceServers : DEFAULT_ICE_SERVERS
202
+ const maxActive = Math.max(4, Number(opts.maxActive) || 32)
203
+ const autoRegisterDiscoveryProviders = opts.autoRegisterDiscoveryProviders !== false
204
+ const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
205
+ /** @type {Map<string, Awaited<ReturnType<typeof createLinkImpl>>>} */
206
+ const links = new Map()
207
+ /** @type {Map<string, Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>>} 按 nodeHash 去重的主动外拨 */
208
+ const inflights = new Map()
209
+ /** @type {Map<string, ReturnType<typeof createBufferedSignalSession>>} 按 connId 索引的信令会话(每条 PC 一个方向) */
210
+ const signalSessions = new Map()
211
+ /** @type {Map<string, Set<string>>} */
212
+ const scopeInterests = new Map()
213
+ /** @type {Map<string, Set<Function>>} */
214
+ const scopeListeners = new Map()
215
+ /** @type {Map<string, Function>} */
216
+ const scopeAuthorizers = new Map()
217
+ /** @type {Set<(nodeHash: string, link: unknown) => void>} */
218
+ const linkUpListeners = new Set()
219
+ /** @type {Set<(nodeHash: string, reason: string) => void>} */
220
+ const linkDownListeners = new Set()
221
+ const recentAdverts = createLruMap(1024)
222
+ let runtimeStarted = false
223
+ let stopAdvert = null
224
+ let stopSignalListener = null
225
+ let overlayRouter = null
226
+
227
+ /**
228
+ * 启动 discovery runtime(advert + signal listener)。
229
+ * @returns {Promise<void>}
230
+ */
231
+ async function ensureDiscoveryRuntime() {
232
+ if (runtimeStarted) return
233
+ runtimeStarted = true
234
+ if (autoRegisterDiscoveryProviders) {
235
+ const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
236
+ if (!providerIds.has('mdns'))
237
+ registerDiscoveryProvider(createMdnsDiscoveryProvider())
238
+ if (!providerIds.has('bt'))
239
+ await import('@stoprocent/noble')
240
+ .then(() => import('../discovery/bt.mjs'))
241
+ .then(bt => registerDiscoveryProvider(bt.createBluetoothDiscoveryProvider()))
242
+ .catch(() => { })
243
+ if (!providerIds.has('nostr'))
244
+ // 测试通过 initNode({ signaling: { relayOverride } }) 注入共享 loopback relay;生产则回落到用户 relay + 默认公网 relay。
245
+ // 新 discovery 栈也必须尊重这层 runtime override,否则 live 双节点测试会各打各的公网 relay。
246
+ registerDiscoveryProvider(createNostrDiscoveryProvider({
247
+ relayUrls: getSignalingRuntimeConfig().relayOverride
248
+ ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
249
+ }))
250
+ }
251
+ if (!listDiscoveryProviders().length) return
252
+ stopSignalListener = await listenSignals(selfTopic, bytes => {
253
+ void handleIncomingSignal(bytes).catch(() => { })
254
+ })
255
+ stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
256
+ type: 'advert',
257
+ body: await buildSignedAdvert(selfTopic, Date.now(), localIdentity),
258
+ }))
259
+ }
260
+
261
+ /**
262
+ * 为单条 PC 创建按 connId 标记的信令会话。出站信令都带上 connId,
263
+ * 使同一对节点的两个方向(各自一条 PC)在信令层互不串扰。
264
+ * @param {string} remoteNodeHash 远端节点 64 hex
265
+ * @param {string} connId 连接标识
266
+ * @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
267
+ */
268
+ function createConnSession(remoteNodeHash, connId) {
269
+ const normalized = normalizeHex64(remoteNodeHash)
270
+ const topic = nodeRendezvousTopic(normalized)
271
+ return createBufferedSignalSession(async message => {
272
+ await sendSignal(topic, normalized, encryptSignalPacket(topic, {
273
+ type: 'signal',
274
+ from: localIdentity.nodeHash,
275
+ connId,
276
+ body: message,
277
+ }))
278
+ })
279
+ }
280
+
281
+ /**
282
+ * 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
283
+ * @param {unknown} body 信令 body
284
+ * @returns {boolean} 是 offer 则 true
285
+ */
286
+ function isOfferSignalBody(body) {
287
+ return !!body && typeof body === 'object'
288
+ && body.type === 'description'
289
+ && body.description?.type === 'offer'
290
+ }
291
+
292
+ /**
293
+ * 保留由较小 nodeHash 发起的那条链(两端对任意一条链算出一致结论),用于双 PC glare 择一。
294
+ * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 链路实例
295
+ * @param {string} remoteNodeHash 远端节点 64 hex
296
+ * @returns {boolean} 本条链应保留则 true
297
+ */
298
+ function linkIsPreferred(link, remoteNodeHash) {
299
+ const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
300
+ return link.initiator ? cmp < 0 : cmp > 0
301
+ }
302
+
303
+ /**
304
+ * 为链路绑定 envelope 派发与 down 回调。
305
+ * @param {string} remoteNodeHash 远端节点 64 hex
306
+ * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 链路实例
307
+ * @returns {void}
308
+ */
309
+ function wireLink(remoteNodeHash, link) {
310
+ link.onEnvelope((envelope, senderNodeHash) => {
311
+ void dispatchEnvelope(senderNodeHash, envelope, link).catch(() => { })
312
+ })
313
+ link.onDown(reason => {
314
+ // 只有关闭的是当前规范链路才对外报 linkDown;双 PC 并存期关掉的败者不应误发 peer-leave。
315
+ const wasCanonical = links.get(remoteNodeHash) === link
316
+ if (!wasCanonical) return
317
+ links.delete(remoteNodeHash)
318
+ for (const listener of linkDownListeners)
319
+ try { listener(remoteNodeHash, reason) } catch { /* ignore */ }
320
+ })
321
+ }
322
+
323
+ /**
324
+ * 注册已建立的链路,并做双 PC glare 择一:同一对节点若因双向同时发起而建成两条 PC,
325
+ * 保留由较小 nodeHash 发起的那条(两端一致),关闭另一条。单条链路(常态)直接安装。
326
+ * @param {string} remoteNodeHash 远端节点 64 hex
327
+ * @param {Awaited<ReturnType<typeof createLinkImpl>>} candidate 候选链路
328
+ * @returns {Promise<void>}
329
+ */
330
+ async function registerResolvedLink(remoteNodeHash, candidate) {
331
+ const normalized = normalizeHex64(remoteNodeHash)
332
+ const existing = links.get(normalized)
333
+ // 已有规范链且候选不优先:候选出局,保留现有。
334
+ if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized)) {
335
+ await candidate.close('glare-loser')
336
+ return
337
+ }
338
+ // 候选胜出(或无现有):先把 candidate 设为规范链,再关旧链——
339
+ // 这样旧链 onDown 时 links.get 已指向 candidate,不会被误判为规范链而对外报 linkDown。
340
+ links.set(normalized, candidate)
341
+ wireLink(normalized, candidate)
342
+ if (existing && existing !== candidate)
343
+ await existing.close('glare-replaced')
344
+ for (const listener of linkUpListeners)
345
+ try { listener(normalized, candidate) } catch { /* ignore */ }
346
+ }
347
+
348
+ /**
349
+ * 为一条 connId 会话建 PC 并走 glare 择一:建 link → 绑 onDown 清 session → ready → registerResolvedLink。
350
+ * initiator 侧建链前先 trimToBudget 腾预算。出错则清理该 connId 会话。
351
+ * @param {{ remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} opts 建链参数
352
+ * @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 当前规范链(本条可能因 glare 被关,取 links 现值);失败 null
353
+ */
354
+ async function buildConnLink({ remoteNodeHash, connId, session, initiator }) {
355
+ try {
356
+ if (initiator) await trimToBudget()
357
+ const link = await createLinkImpl({ nodeHash: remoteNodeHash, initiator, signal: session, iceServers, localIdentity })
358
+ link.onDown(() => signalSessions.delete(connId))
359
+ await link.ready
360
+ await registerResolvedLink(remoteNodeHash, link)
361
+ return links.get(remoteNodeHash) ?? null
362
+ }
363
+ catch {
364
+ signalSessions.delete(connId)
365
+ return null
366
+ }
367
+ }
368
+
369
+ /**
370
+ * 处理入站加密信令并可能发起被动建链。
371
+ * @param {Uint8Array} bytes 加密信令字节
372
+ * @returns {Promise<void>}
373
+ */
374
+ async function handleIncomingSignal(bytes) {
375
+ const packet = decryptSignalPacket(selfTopic, bytes)
376
+ if (!packet || packet.type !== 'signal') return
377
+ const remoteNodeHash = normalizeHex64(packet.from)
378
+ const connId = String(packet.connId || '')
379
+ if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
380
+ let session = signalSessions.get(connId)
381
+ if (!session) {
382
+ // 只有全新的 offer 才开一条独立应答 PC;answer/ice 若无对应 connId 会话则是迟到/无效帧,丢弃。
383
+ // 应答 PC 按 connId 独立创建,不受按 nodeHash 去重的 inflight 阻挡——这是支持双向同时建链的关键。
384
+ if (!isOfferSignalBody(packet.body)) return
385
+ session = createConnSession(remoteNodeHash, connId)
386
+ signalSessions.set(connId, session)
387
+ void buildConnLink({ remoteNodeHash, connId, session, initiator: false })
388
+ }
389
+ session.deliver(packet.body)
390
+ }
391
+
392
+ /**
393
+ * 计算节点在 scope 兴趣中的权重(用于 eviction)。
394
+ * @param {string} remoteNodeHash 远端节点 64 hex
395
+ * @returns {number} 权重值
396
+ */
397
+ function scopeWeight(remoteNodeHash) {
398
+ let weight = 0
399
+ for (const hashes of scopeInterests.values())
400
+ if (hashes.has(remoteNodeHash)) weight++
401
+ return weight
402
+ }
403
+
404
+ /**
405
+ * 超出 maxActive 时驱逐权重最低的链路。
406
+ * @returns {Promise<void>}
407
+ */
408
+ async function trimToBudget() {
409
+ if (links.size < maxActive) return
410
+ const candidates = [...links.entries()]
411
+ .sort((left, right) => scopeWeight(left[0]) - scopeWeight(right[0]) || compareHex64Asc(left[0], right[0]))
412
+ const victim = candidates[0]
413
+ if (victim) await victim[1].close('budget-evict')
414
+ }
415
+
416
+ /**
417
+ * 主动建链到远端节点(initiator)。
418
+ * @param {string} remoteNodeHash 远端节点 64 hex
419
+ * @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 链路实例;失败时 null
420
+ */
421
+ async function ensureDirectLinkToNode(remoteNodeHash) {
422
+ await ensureDiscoveryRuntime()
423
+ const normalized = normalizeHex64(remoteNodeHash)
424
+ if (!normalized || normalized === localIdentity.nodeHash) return null
425
+ if (links.has(normalized)) return links.get(normalized)
426
+ if (inflights.has(normalized)) return await inflights.get(normalized)
427
+ // 想连就直拨(单向零额外成本);若对端也在同时拨,双方各建一条 PC,由 registerResolvedLink 择一。
428
+ // buildConnLink 成功时返回的是规范链而非本条 candidate——glare 双 PC 择一时本条可能是被关闭的败者。
429
+ const connId = randomBytes(16).toString('hex')
430
+ const session = createConnSession(normalized, connId)
431
+ signalSessions.set(connId, session)
432
+ const task = buildConnLink({ remoteNodeHash: normalized, connId, session, initiator: true })
433
+ .finally(() => inflights.delete(normalized))
434
+ inflights.set(normalized, task)
435
+ return await task
436
+ }
437
+
438
+ /**
439
+ * 经已有直连发送 envelope。
440
+ * @param {string} remoteNodeHash 远端节点 64 hex
441
+ * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
442
+ * @returns {Promise<boolean>} 是否发送成功
443
+ */
444
+ async function sendDirectToNodeLink(remoteNodeHash, envelope) {
445
+ const normalized = normalizeHex64(remoteNodeHash)
446
+ if (!normalized) return false
447
+ const link = links.get(normalized)
448
+ if (!link) return false
449
+ try {
450
+ return await link.send(envelope)
451
+ }
452
+ catch {
453
+ return false
454
+ }
455
+ }
456
+
457
+ /**
458
+ * 懒创建 overlay 路由器。
459
+ * @returns {ReturnType<typeof createOverlayRouter>} overlay 路由器
460
+ */
461
+ function getOverlayRouter() {
462
+ if (overlayRouter) return overlayRouter
463
+ overlayRouter = createOverlayRouter({
464
+ localIdentity,
465
+ sendToNodeLink: sendDirectToNodeLink,
466
+ /**
467
+ * 列出当前所有活跃链路。
468
+ * @returns {Array<{ nodeHash: string, link: Awaited<ReturnType<typeof createLinkImpl>> }>} 链路列表
469
+ */
470
+ listLinks() {
471
+ return [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link }))
472
+ },
473
+ subscribeScope,
474
+ })
475
+ overlayRouter.onRelay((body, meta) => {
476
+ void dispatchEnvelope(meta.path[0], body, null).catch(() => { })
477
+ })
478
+ return overlayRouter
479
+ }
480
+
481
+ /**
482
+ * 经 overlay 多跳 relay envelope 到无直连的节点。
483
+ * @param {string} remoteNodeHash 远端节点 64 hex
484
+ * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
485
+ * @returns {Promise<boolean>} 是否 relay 成功
486
+ */
487
+ async function relayEnvelopeToNode(remoteNodeHash, envelope) {
488
+ if (!links.size || envelope?.scope === 'overlay') return false
489
+ try {
490
+ const path = await getOverlayRouter().discoverRoute(remoteNodeHash)
491
+ await getOverlayRouter().relay(path, envelope)
492
+ return true
493
+ }
494
+ catch {
495
+ return false
496
+ }
497
+ }
498
+
499
+ /**
500
+ * 确保到远端节点的链路(直连或被动应答)。
501
+ * @param {string} remoteNodeHash 远端节点 64 hex
502
+ * @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 链路实例
503
+ */
504
+ async function ensureLinkToNode(remoteNodeHash) {
505
+ return await ensureDirectLinkToNode(remoteNodeHash)
506
+ }
507
+
508
+ /**
509
+ * 发送 envelope:优先直连,失败则 overlay relay。
510
+ * @param {string} remoteNodeHash 远端节点 64 hex
511
+ * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
512
+ * @returns {Promise<boolean>} 是否发送成功
513
+ */
514
+ async function sendToNodeLink(remoteNodeHash, envelope) {
515
+ return await sendDirectToNodeLink(remoteNodeHash, envelope)
516
+ || await relayEnvelopeToNode(remoteNodeHash, envelope)
517
+ }
518
+
519
+ /**
520
+ * 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
521
+ * @param {string} senderNodeHash 发送方节点 64 hex
522
+ * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
523
+ * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 来源链路
524
+ * @returns {Promise<void>}
525
+ */
526
+ async function dispatchEnvelope(senderNodeHash, envelope, link) {
527
+ const scope = String(envelope?.scope || '')
528
+ for (const [prefix, authorizer] of scopeAuthorizers.entries())
529
+ if (scope.startsWith(prefix)) {
530
+ const allowed = await Promise.resolve(authorizer(scope, senderNodeHash, envelope, link))
531
+ if (!allowed) return
532
+ }
533
+ for (const [prefix, listeners] of scopeListeners.entries())
534
+ if (scope.startsWith(prefix))
535
+ for (const listener of listeners)
536
+ await Promise.resolve(listener(senderNodeHash, envelope, link))
537
+ }
538
+
539
+ /**
540
+ * 订阅指定 scope 前缀的 envelope。
541
+ * @param {string} prefix scope 前缀
542
+ * @param {(senderNodeHash: string, envelope: { scope: string, action: string, payload: unknown }, link: Awaited<ReturnType<typeof createLinkImpl>>) => void | Promise<void>} listener 监听器
543
+ * @returns {() => void} 取消订阅函数
544
+ */
545
+ function subscribeScope(prefix, listener) {
546
+ return subscribeBucket(scopeListeners, String(prefix), listener)
547
+ }
548
+
549
+ /**
550
+ * 注册 scope 前缀的 authorizer(入站校验)。
551
+ * @param {string} prefix scope 前缀
552
+ * @param {(scope: string, senderNodeHash: string, envelope: object, link: Awaited<ReturnType<typeof createLinkImpl>>) => boolean | Promise<boolean>} authorizer 校验函数
553
+ * @returns {() => void} 取消注册函数
554
+ */
555
+ function registerScopeAuthorizer(prefix, authorizer) {
556
+ scopeAuthorizers.set(String(prefix), authorizer)
557
+ return () => scopeAuthorizers.delete(String(prefix))
558
+ }
559
+
560
+ return {
561
+ localIdentity,
562
+ ensureRuntime: ensureDiscoveryRuntime,
563
+ ensureLinkToNode,
564
+ /**
565
+ * 获取到指定节点的活跃链路。
566
+ * @param {string} nodeHash 节点 64 hex
567
+ * @returns {Awaited<ReturnType<typeof createLinkImpl>> | null} 链路实例;不存在时 null
568
+ */
569
+ getLink(nodeHash) {
570
+ return links.get(normalizeHex64(nodeHash)) || null
571
+ },
572
+ /**
573
+ * 列出所有活跃链路。
574
+ * @returns {Array<{ nodeHash: string, link: Awaited<ReturnType<typeof createLinkImpl>> }>} 链路列表
575
+ */
576
+ listLinks() {
577
+ return [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link }))
578
+ },
579
+ /**
580
+ * 关闭到指定节点的链路。
581
+ * @param {string} nodeHash 节点 64 hex
582
+ * @param {string} [reason='manual-close'] 关闭原因
583
+ * @returns {Promise<void>}
584
+ */
585
+ async closeLink(nodeHash, reason = 'manual-close') {
586
+ const normalized = normalizeHex64(nodeHash)
587
+ const link = links.get(normalized)
588
+ if (!link) return
589
+ await link.close(reason)
590
+ },
591
+ sendToNodeLink,
592
+ /**
593
+ * 订阅链路建立事件。
594
+ * @param {(nodeHash: string, link: unknown) => void} listener 回调
595
+ * @returns {() => void} 取消订阅函数
596
+ */
597
+ onLinkUp(listener) {
598
+ linkUpListeners.add(listener)
599
+ return () => linkUpListeners.delete(listener)
600
+ },
601
+ /**
602
+ * 订阅链路断开事件。
603
+ * @param {(nodeHash: string, reason: string) => void} listener 回调
604
+ * @returns {() => void} 取消订阅函数
605
+ */
606
+ onLinkDown(listener) {
607
+ linkDownListeners.add(listener)
608
+ return () => linkDownListeners.delete(listener)
609
+ },
610
+ /**
611
+ * 注册 scope 兴趣成员(影响 eviction 权重)。
612
+ * @param {string} scope scope 名称
613
+ * @param {string[]} nodeHashes 成员 nodeHash 列表
614
+ * @returns {void}
615
+ */
616
+ registerScopeInterest(scope, nodeHashes) {
617
+ scopeInterests.set(String(scope), new Set((Array.isArray(nodeHashes) ? nodeHashes : []).map(normalizeHex64).filter(Boolean)))
618
+ },
619
+ /**
620
+ * 释放 scope 兴趣。
621
+ * @param {string} scope scope 名称
622
+ * @returns {void}
623
+ */
624
+ releaseScopeInterest(scope) {
625
+ scopeInterests.delete(String(scope))
626
+ },
627
+ registerScopeAuthorizer,
628
+ subscribeScope,
629
+ /**
630
+ * 订阅指定节点的 advert 广播。
631
+ * @param {string} nodeHash 目标节点 64 hex
632
+ * @param {(verifiedNodeHash: string, body: object) => void | Promise<void>} onAdvert advert 回调
633
+ * @returns {Promise<() => void>} 取消订阅函数
634
+ */
635
+ async subscribeNodeAdvert(nodeHash, onAdvert) {
636
+ const topic = nodeRendezvousTopic(nodeHash)
637
+ return await subscribeTopic(topic, async bytes => {
638
+ const packet = decryptSignalPacket(topic, bytes)
639
+ if (packet?.type !== 'advert') return
640
+ const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
641
+ if (!verifiedNodeHash) return
642
+ recentAdverts.touch(verifiedNodeHash, Date.now())
643
+ await Promise.resolve(onAdvert(verifiedNodeHash, packet.body))
644
+ })
645
+ },
646
+ recentAdverts,
647
+ relayEnvelopeToNode,
648
+ /**
649
+ * 关闭 registry:停止 discovery、overlay 并断开所有链路。
650
+ * @returns {Promise<void>}
651
+ */
652
+ async shutdown() {
653
+ stopAdvert?.()
654
+ stopSignalListener?.()
655
+ overlayRouter?.close()
656
+ overlayRouter = null
657
+ for (const link of links.values())
658
+ await link.close('registry-shutdown')
659
+ links.clear()
660
+ inflights.clear()
661
+ for (const session of signalSessions.values()) session.clear()
662
+ signalSessions.clear()
663
+ },
664
+ }
665
+ }
666
+
667
+ let defaultRegistry = null
668
+
669
+ /**
670
+ * 获取进程级默认 link registry 单例。
671
+ * @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
672
+ */
673
+ export function getLinkRegistry() {
674
+ if (!defaultRegistry) defaultRegistry = createLinkRegistry()
675
+ return defaultRegistry
676
+ }
677
+
678
+ /**
679
+ * 默认 registry 的 ensureLinkToNode 代理。
680
+ * @param {...any} args 转发参数
681
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['ensureLinkToNode']>} 链路实例
682
+ */
683
+ export const ensureLinkToNode = (...args) => getLinkRegistry().ensureLinkToNode(...args)
684
+ /**
685
+ * 默认 registry 的 getLink 代理。
686
+ * @param {...any} args 转发参数
687
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['getLink']>} 链路实例
688
+ */
689
+ export const getLink = (...args) => getLinkRegistry().getLink(...args)
690
+ /**
691
+ * 默认 registry 的 listLinks 代理。
692
+ * @param {...any} args 转发参数
693
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['listLinks']>} 链路列表
694
+ */
695
+ export const listLinks = (...args) => getLinkRegistry().listLinks(...args)
696
+ /**
697
+ * 默认 registry 的 closeLink 代理。
698
+ * @param {...any} args 转发参数
699
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['closeLink']>} 关闭完成
700
+ */
701
+ export const closeLink = (...args) => getLinkRegistry().closeLink(...args)
702
+ /**
703
+ * 默认 registry 的 sendToNodeLink 代理。
704
+ * @param {...any} args 转发参数
705
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['sendToNodeLink']>} 是否成功
706
+ */
707
+ export const sendToNodeLink = (...args) => getLinkRegistry().sendToNodeLink(...args)
708
+ /**
709
+ * 默认 registry 的 relayEnvelopeToNode 代理。
710
+ * @param {...any} args 转发参数
711
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['relayEnvelopeToNode']>} 是否成功
712
+ */
713
+ export const relayEnvelopeToNode = (...args) => getLinkRegistry().relayEnvelopeToNode(...args)
714
+ /**
715
+ * 默认 registry 的 onLinkUp 代理。
716
+ * @param {...any} args 转发参数
717
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkUp']>} 取消订阅函数
718
+ */
719
+ export const onLinkUp = (...args) => getLinkRegistry().onLinkUp(...args)
720
+ /**
721
+ * 默认 registry 的 onLinkDown 代理。
722
+ * @param {...any} args 转发参数
723
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkDown']>} 取消订阅函数
724
+ */
725
+ export const onLinkDown = (...args) => getLinkRegistry().onLinkDown(...args)
726
+ /**
727
+ * 默认 registry 的 registerScopeInterest 代理。
728
+ * @param {...any} args 转发参数
729
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeInterest']>} 无返回值
730
+ */
731
+ export const registerScopeInterest = (...args) => getLinkRegistry().registerScopeInterest(...args)
732
+ /**
733
+ * 默认 registry 的 releaseScopeInterest 代理。
734
+ * @param {...any} args 转发参数
735
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['releaseScopeInterest']>} 无返回值
736
+ */
737
+ export const releaseScopeInterest = (...args) => getLinkRegistry().releaseScopeInterest(...args)
738
+ /**
739
+ * 默认 registry 的 registerScopeAuthorizer 代理。
740
+ * @param {...any} args 转发参数
741
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeAuthorizer']>} 取消注册函数
742
+ */
743
+ export const registerScopeAuthorizer = (...args) => getLinkRegistry().registerScopeAuthorizer(...args)
744
+ /**
745
+ * 默认 registry 的 subscribeScope 代理。
746
+ * @param {...any} args 转发参数
747
+ * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['subscribeScope']>} 取消订阅函数
748
+ */
749
+ export const subscribeScope = (...args) => getLinkRegistry().subscribeScope(...args)