@steve02081504/fount-p2p 0.0.26 → 0.0.28

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 (100) hide show
  1. package/README.md +3 -2
  2. package/core/bytes_codec.mjs +5 -6
  3. package/core/composite_key.mjs +0 -79
  4. package/core/entity_id_parse.mjs +2 -3
  5. package/core/evfs_logical_path.mjs +2 -3
  6. package/core/hexIds.mjs +5 -5
  7. package/core/logical_entity.mjs +3 -4
  8. package/crypto/channel.mjs +2 -25
  9. package/crypto/checkpoint_sign.mjs +5 -4
  10. package/crypto/crypto.mjs +0 -8
  11. package/crypto/key.mjs +9 -10
  12. package/dag/canonicalize_row.mjs +1 -1
  13. package/discovery/advert_peer_hints.mjs +1 -1
  14. package/discovery/adverts.mjs +7 -12
  15. package/discovery/bt/index.mjs +6 -11
  16. package/discovery/bt/peer_hints.mjs +2 -3
  17. package/discovery/bt/runtime.mjs +1 -1
  18. package/discovery/index.mjs +14 -14
  19. package/discovery/lan.mjs +3 -3
  20. package/discovery/lan_interfaces.mjs +2 -2
  21. package/discovery/lan_peer_hints.mjs +1 -1
  22. package/discovery/nostr.mjs +19 -24
  23. package/federation/entity_key_chain.mjs +5 -24
  24. package/federation/message_rate_limit.mjs +3 -2
  25. package/federation/part_query/runtime.mjs +9 -20
  26. package/files/assemble.mjs +3 -3
  27. package/files/assemble_stream.mjs +1 -1
  28. package/files/chunk/fetch.mjs +1 -2
  29. package/files/chunk/responder.mjs +4 -41
  30. package/files/chunk/store.mjs +2 -3
  31. package/files/chunk/verify.mjs +2 -3
  32. package/files/evfs.mjs +1 -1
  33. package/files/evfs_ref.mjs +3 -4
  34. package/files/fetch_fanout.mjs +3 -3
  35. package/files/manifest/fetch.mjs +2 -3
  36. package/files/manifest/normalize.mjs +9 -9
  37. package/files/manifest/pending.mjs +1 -4
  38. package/files/manifest/public.mjs +8 -8
  39. package/files/transfer_key.mjs +3 -3
  40. package/files/transfer_key_registry.mjs +6 -6
  41. package/governance/branch.mjs +2 -1
  42. package/governance/join_pow.mjs +7 -5
  43. package/governance/owner_succession_ballot.mjs +6 -6
  44. package/index.mjs +8 -2
  45. package/infra/default_node_dir.mjs +2 -2
  46. package/infra/priority.mjs +2 -2
  47. package/link/frame.mjs +2 -3
  48. package/link/handshake.mjs +6 -6
  49. package/link/pipe.mjs +69 -5
  50. package/link/providers/ble_gatt.mjs +1 -9
  51. package/link/providers/index.mjs +2 -2
  52. package/link/providers/nostr.mjs +1 -3
  53. package/link/providers/webrtc.mjs +2 -2
  54. package/link/rtc/ice_local_hostname.mjs +6 -5
  55. package/link/rtc/w3c_bridge.mjs +1 -1
  56. package/link/sdp_fingerprint.mjs +1 -1
  57. package/mailbox/consumer_registry.mjs +2 -2
  58. package/mailbox/deliver_or_store.mjs +4 -4
  59. package/mailbox/parse.mjs +1 -1
  60. package/mailbox/prune.mjs +2 -2
  61. package/mailbox/rate.mjs +3 -10
  62. package/mailbox/store.mjs +7 -7
  63. package/node/denylist.mjs +38 -38
  64. package/node/entity_store.mjs +5 -5
  65. package/node/identity.mjs +7 -8
  66. package/node/instance.mjs +1 -1
  67. package/node/log.mjs +1 -1
  68. package/node/network.mjs +14 -16
  69. package/node/personal_block.mjs +22 -20
  70. package/node/reputation_store.mjs +11 -19
  71. package/node/reputation_sync.mjs +0 -7
  72. package/node/signaling_config.mjs +1 -1
  73. package/node/storage_plugins.mjs +2 -2
  74. package/overlay/index.mjs +1 -1
  75. package/package.json +1 -1
  76. package/registries/event_type.mjs +2 -2
  77. package/registries/room_provider.mjs +2 -2
  78. package/reputation/engine.mjs +17 -15
  79. package/schemas/discovery.mjs +5 -5
  80. package/schemas/federation_pull.mjs +16 -24
  81. package/schemas/part_query.mjs +4 -4
  82. package/timeline/verify_remote.mjs +4 -4
  83. package/transport/group_link_set.mjs +3 -3
  84. package/transport/ice_servers.mjs +2 -2
  85. package/transport/link_registry.mjs +51 -20
  86. package/transport/mesh_keepalive.mjs +1 -1
  87. package/transport/node_scope/wire.mjs +1 -1
  88. package/transport/peer_health.mjs +127 -0
  89. package/transport/remote_user_room.mjs +13 -15
  90. package/transport/rtc_connection_budget.mjs +6 -6
  91. package/transport/runtime_bootstrap.mjs +12 -12
  92. package/trust_graph/cache.mjs +2 -3
  93. package/trust_graph/engine.mjs +1 -1
  94. package/trust_graph/registry.mjs +2 -2
  95. package/trust_graph/send.mjs +1 -1
  96. package/utils/async_mutex.mjs +1 -1
  97. package/wire/ingress.mjs +1 -1
  98. package/wire/part/ingress.mjs +6 -6
  99. package/wire/part/query.mjs +3 -3
  100. package/wire/volatile_signature.mjs +4 -9
package/overlay/index.mjs CHANGED
@@ -15,7 +15,7 @@ let overlayRateGate = null
15
15
  * @returns {void}
16
16
  */
17
17
  export function setOverlayRateGate(rateGate) {
18
- overlayRateGate = typeof rateGate === 'function' ? rateGate : null
18
+ overlayRateGate = rateGate || null
19
19
  }
20
20
 
21
21
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -15,7 +15,7 @@ const defsByOwner = new Map()
15
15
  * @returns {void}
16
16
  */
17
17
  export function registerEventTypeDefs(ownerId, defs) {
18
- defsByOwner.set(String(ownerId), defs)
18
+ defsByOwner.set(ownerId, defs)
19
19
  }
20
20
 
21
21
  /**
@@ -23,7 +23,7 @@ export function registerEventTypeDefs(ownerId, defs) {
23
23
  * @returns {void}
24
24
  */
25
25
  export function unregisterEventTypeDefs(ownerId) {
26
- defsByOwner.delete(String(ownerId))
26
+ defsByOwner.delete(ownerId)
27
27
  }
28
28
 
29
29
  /** @returns {void} */
@@ -21,7 +21,7 @@ const providers = new Map()
21
21
  * @returns {void}
22
22
  */
23
23
  export function registerFederationRoomProvider(ownerId, enumerateRooms) {
24
- providers.set(String(ownerId), enumerateRooms)
24
+ providers.set(ownerId, enumerateRooms)
25
25
  }
26
26
 
27
27
  /**
@@ -29,7 +29,7 @@ export function registerFederationRoomProvider(ownerId, enumerateRooms) {
29
29
  * @returns {void}
30
30
  */
31
31
  export function unregisterFederationRoomProvider(ownerId) {
32
- providers.delete(String(ownerId))
32
+ providers.delete(ownerId)
33
33
  }
34
34
 
35
35
  /**
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * 信誉纯内存算子(模拟器与磁盘 store 共用)。
3
3
  */
4
+ import { normalizeHex64 } from '../core/hexIds.mjs'
5
+
4
6
  import {
5
7
  clampReputationScore,
6
8
  computeRepMaxEff,
@@ -176,9 +178,9 @@ export function pruneReputationFile(data, tunables = reputationTunables, now = D
176
178
  * @returns {boolean} 是否已加分
177
179
  */
178
180
  export function bumpReputationOnRelayPure(data, peerNodeHash, dedupeKey, now = Date.now(), tunables = reputationTunables) {
179
- const id = String(peerNodeHash || '').trim()
181
+ const id = normalizeHex64(peerNodeHash)
180
182
  if (!id) return false
181
- const key = String(dedupeKey || `conn:${id}`).trim()
183
+ const key = dedupeKey || `conn:${id}`
182
184
  data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
183
185
  if (relayBumpIsDuplicate(data.relayBumpSeen, id, key, now, tunables.relayBumpDedupeMs)) return false
184
186
  data.relayBumpSeen.push({ peerNodeHash: id, key, t: now })
@@ -213,7 +215,7 @@ export function recordGossipAllUnknownWantPure(data, peerNodeHash, now = Date.no
213
215
  * @returns {void}
214
216
  */
215
217
  export function recordMessageRateViolationPure(data, peerNodeHash, tunables = reputationTunables, excessRatio = 1) {
216
- const id = String(peerNodeHash || '').trim()
218
+ const id = normalizeHex64(peerNodeHash)
217
219
  if (!id) return
218
220
  const ratio = Math.max(0, Math.min(1, Number(excessRatio) || 0))
219
221
  if (ratio <= 0) return
@@ -227,7 +229,7 @@ export function recordMessageRateViolationPure(data, peerNodeHash, tunables = re
227
229
  * @returns {void}
228
230
  */
229
231
  export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables = reputationTunables) {
230
- const id = String(storagePeerKey || '').trim()
232
+ const id = normalizeHex64(storagePeerKey)
231
233
  if (!id) return
232
234
  adjustNodeReputation(data, id, tunables.chunkStoreRepBump)
233
235
  }
@@ -239,7 +241,7 @@ export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables =
239
241
  * @returns {void}
240
242
  */
241
243
  export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = reputationTunables) {
242
- const id = String(blamePeerKey || '').trim()
244
+ const id = normalizeHex64(blamePeerKey)
243
245
  if (!id) return
244
246
  adjustNodeReputation(data, id, -tunables.chunkFetchFailPenalty)
245
247
  }
@@ -251,7 +253,7 @@ export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = r
251
253
  * @returns {void}
252
254
  */
253
255
  export function penalizeArchiveServeMismatchPure(data, peerNodeHash, tunables = reputationTunables) {
254
- const id = String(peerNodeHash || '').trim()
256
+ const id = normalizeHex64(peerNodeHash)
255
257
  if (!id) return
256
258
  adjustNodeReputation(data, id, -tunables.archiveServeMismatchPenalty)
257
259
  }
@@ -280,7 +282,7 @@ export function applySubjectiveSlashPure(data, target, sender, claim, verified,
280
282
  * @returns {Array<{ hop: number, node: string, dRep: number }>} 已应用的上游惩罚
281
283
  */
282
284
  export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges, tunables = reputationTunables) {
283
- const target = targetPubKeyHash.trim().toLowerCase()
285
+ const target = normalizeHex64(targetPubKeyHash)
284
286
  /** @type {Array<{ hop: number, node: string, dRep: number }>} */
285
287
  const applied = []
286
288
  let frontier = new Set([target])
@@ -301,8 +303,8 @@ export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, invite
301
303
  for (let hop = 1; hop <= effectiveMaxHop(); hop++) {
302
304
  const upstream = new Set()
303
305
  for (const edge of inviteEdges) {
304
- const to = String(edge.to ?? '').trim().toLowerCase()
305
- const from = String(edge.from ?? '').trim().toLowerCase()
306
+ const to = normalizeHex64(edge.to)
307
+ const from = normalizeHex64(edge.from)
306
308
  if (frontier.has(to) && from) upstream.add(from)
307
309
  }
308
310
  if (!upstream.size) break
@@ -331,7 +333,7 @@ export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, invite
331
333
  * @returns {void}
332
334
  */
333
335
  export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
334
- const t = targetPubKeyHash.trim().toLowerCase()
336
+ const t = normalizeHex64(targetPubKeyHash)
335
337
  const row = repRow(data, t)
336
338
  row.score = 0
337
339
  delete row.offenseStreak
@@ -351,8 +353,8 @@ export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
351
353
  * @returns {void}
352
354
  */
353
355
  export function seedMemberReputationFromIntroducerPure(data, memberPubKeyHash, introducerPubKeyHash, repEdge, tunables = reputationTunables, powBonus = 0) {
354
- const memberKey = memberPubKeyHash.trim().toLowerCase()
355
- const introducerKey = introducerPubKeyHash?.trim().toLowerCase() || ''
356
+ const memberKey = normalizeHex64(memberPubKeyHash)
357
+ const introducerKey = normalizeHex64(introducerPubKeyHash)
356
358
  if (data.byNodeHash[memberKey]) return
357
359
  const introducerReputation = introducerKey
358
360
  ? Number(data.byNodeHash[introducerKey]?.score ?? 0)
@@ -392,7 +394,7 @@ function baselineRow(row) {
392
394
  * @returns {{ z: number, anomaly: boolean }} 偏离度
393
395
  */
394
396
  export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date.now(), tunables = reputationTunables) {
395
- const id = String(peerNodeHash || '').trim()
397
+ const id = normalizeHex64(peerNodeHash)
396
398
  if (!id) return { z: 0, anomaly: false }
397
399
  const row = repRow(data, id)
398
400
  const baseline = baselineRow(row)
@@ -427,7 +429,7 @@ export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date
427
429
  * @returns {void}
428
430
  */
429
431
  export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
430
- const id = String(peerNodeHash || '').trim()
432
+ const id = normalizeHex64(peerNodeHash)
431
433
  if (!id) return
432
434
  const row = repRow(data, id)
433
435
  row.quarantinedUntil = now + Number(tunables.quarantineTtlMs ?? 900_000)
@@ -441,7 +443,7 @@ export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunabl
441
443
  * @returns {boolean} 是否处于本地隔离
442
444
  */
443
445
  export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
444
- const id = String(peerNodeHash || '').trim()
446
+ const id = normalizeHex64(peerNodeHash)
445
447
  const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
446
448
  return Number.isFinite(until) && until > now
447
449
  }
@@ -9,9 +9,9 @@ const MAX_DISCOVERY_ADS = 64
9
9
  */
10
10
  function sanitizeDiscoveryAdvertisement(ad) {
11
11
  if (!isPlainObject(ad)) return null
12
- const groupId = String(ad.groupId || '').trim()
12
+ const groupId = String(ad.groupId || '')
13
13
  const advertiserPubKeyHash = normalizeHex64(ad.advertiserPubKeyHash)
14
- const signature = String(ad.signature || '').trim().toLowerCase()
14
+ const signature = String(ad.signature || '')
15
15
  if (!groupId || !isHex64(advertiserPubKeyHash) || !isSignatureHex128(signature)) return null
16
16
  const advertiserNodeHash = normalizeHex64(ad.advertiserNodeHash)
17
17
  const body = {
@@ -19,7 +19,7 @@ function sanitizeDiscoveryAdvertisement(ad) {
19
19
  title: String(ad.title || '').slice(0, 200),
20
20
  blurb: String(ad.blurb || '').slice(0, 500),
21
21
  advertiserPubKeyHash,
22
- advertiserNodeHash: isHex64(advertiserNodeHash) ? advertiserNodeHash : String(ad.advertiserNodeHash || '').trim(),
22
+ advertiserNodeHash: isHex64(advertiserNodeHash) ? advertiserNodeHash : String(ad.advertiserNodeHash || ''),
23
23
  observedAt: Number(ad.observedAt) || 0,
24
24
  signature,
25
25
  }
@@ -47,10 +47,10 @@ export function assertDiscoveryNodeHash(nodeHash) {
47
47
 
48
48
  /**
49
49
  * @param {unknown} requestId 请求 id
50
- * @returns {string} 非空 trimmed 字符串
50
+ * @returns {string} 非空字符串
51
51
  */
52
52
  export function assertDiscoveryRequestId(requestId) {
53
- const id = String(requestId ?? '').trim()
53
+ const id = String(requestId ?? '')
54
54
  if (!id) throw new Error('discovery.requestId required')
55
55
  return id
56
56
  }
@@ -14,7 +14,7 @@ const ENVELOPE_BLOB_MIN_LEN = 2
14
14
  * @returns {boolean} 是否像合法 ECIES blob 段
15
15
  */
16
16
  function isEnvelopeBlob(value) {
17
- return String(value ?? '').trim().length >= ENVELOPE_BLOB_MIN_LEN
17
+ return String(value ?? '').length >= ENVELOPE_BLOB_MIN_LEN
18
18
  }
19
19
 
20
20
  /**
@@ -24,14 +24,14 @@ function isEnvelopeBlob(value) {
24
24
  export function parsePullAttestation(attestation) {
25
25
  if (!isPlainObject(attestation)) return null
26
26
  const requesterPubKeyHash = normalizeHex64(attestation.requesterPubKeyHash)
27
- const groupId = String(attestation.groupId || '').trim()
28
- const requestId = String(attestation.requestId || '').trim()
27
+ const groupId = String(attestation.groupId || '')
28
+ const requestId = String(attestation.requestId || '')
29
29
  const timestamp = Number(attestation.timestamp)
30
- const signature = String(attestation.signature || '').trim().toLowerCase()
30
+ const signature = String(attestation.signature || '')
31
31
  if (!isHex64(requesterPubKeyHash) || !groupId || !Number.isFinite(timestamp) || !isSignatureHex128(signature))
32
32
  return null
33
33
  const wantIds = attestation.wantIds?.length
34
- ? [...new Set(attestation.wantIds.map(id => id.trim().toLowerCase()).filter(id => EVENT_ID_HEX.test(id)))]
34
+ ? [...new Set(attestation.wantIds.map(id => normalizeHex64(id)).filter(id => EVENT_ID_HEX.test(id)))]
35
35
  : undefined
36
36
  return {
37
37
  requesterPubKeyHash,
@@ -49,13 +49,13 @@ export function parsePullAttestation(attestation) {
49
49
  */
50
50
  export function parsePullResponseEnvelope(envelope) {
51
51
  if (!isPlainObject(envelope)) return null
52
- const requestId = String(envelope.requestId || '').trim()
52
+ const requestId = String(envelope.requestId || '')
53
53
  const requesterPubKeyHash = normalizeHex64(envelope.requesterPubKeyHash)
54
- const requesterNodeHash = String(envelope.requesterNodeHash || '').trim()
55
- const ephemPub = String(envelope.ephemPub || '').trim()
56
- const iv = String(envelope.iv || '').trim()
57
- const ciphertext = String(envelope.ciphertext || '').trim()
58
- const authTag = String(envelope.authTag || '').trim()
54
+ const requesterNodeHash = String(envelope.requesterNodeHash || '')
55
+ const ephemPub = String(envelope.ephemPub || '')
56
+ const iv = String(envelope.iv || '')
57
+ const ciphertext = String(envelope.ciphertext || '')
58
+ const authTag = String(envelope.authTag || '')
59
59
  if (!requestId || !isHex64(requesterPubKeyHash) || !requesterNodeHash) return null
60
60
  if (!isEnvelopeBlob(ephemPub) || !isEnvelopeBlob(iv) || !isEnvelopeBlob(ciphertext) || !isEnvelopeBlob(authTag))
61
61
  return null
@@ -76,27 +76,19 @@ export function parsePullResponseEnvelope(envelope) {
76
76
  */
77
77
  export function parseJoinSnapshotRequest(data) {
78
78
  if (!isPlainObject(data)) return null
79
- const requestId = String(data.requestId || '').trim()
80
- const requesterNodeHash = String(data.requesterNodeHash || '').trim()
81
- const groupId = String(data.groupId || '').trim()
79
+ const requestId = String(data.requestId || '')
80
+ const requesterNodeHash = String(data.requesterNodeHash || '')
81
+ const groupId = String(data.groupId || '')
82
82
  const attestation = parsePullAttestation(data.attestation)
83
83
  if (!requestId || !requesterNodeHash || !groupId || !attestation) return null
84
84
  if (attestation.groupId !== groupId || attestation.requestId !== requestId) return null
85
- const tipsHash = String(data.tipsHash || '').trim()
85
+ const tipsHash = String(data.tipsHash || '')
86
86
  return {
87
87
  requestId,
88
88
  requesterNodeHash,
89
89
  requesterPubKeyHash: attestation.requesterPubKeyHash,
90
90
  groupId,
91
- tipsHash: tipsHash ? tipsHash.toLowerCase() : undefined,
91
+ tipsHash: tipsHash || undefined,
92
92
  attestation,
93
93
  }
94
94
  }
95
-
96
- /**
97
- * @param {unknown} data 入群快照响应载荷
98
- * @returns {object | null} 解析结果
99
- */
100
- export function parseJoinSnapshotResponse(data) {
101
- return parsePullResponseEnvelope(data)
102
- }
@@ -82,10 +82,10 @@ const REQUEST_ID_MAX_LENGTH = 128
82
82
 
83
83
  /**
84
84
  * @param {unknown} value 原始 requestId
85
- * @returns {string | null} 修剪后的 requestId;非法 null
85
+ * @returns {string | null} 合法 requestId;非法 null
86
86
  */
87
87
  function normalizeRequestId(value) {
88
- const requestId = String(value || '').trim()
88
+ const requestId = String(value || '')
89
89
  if (!requestId || requestId.length > REQUEST_ID_MAX_LENGTH) return null
90
90
  return requestId
91
91
  }
@@ -103,7 +103,7 @@ export function parsePartQueryReq(value, tunables = partQueryTunables) {
103
103
  if (!isHex64(originNodeHash)) return null
104
104
  const partpath = parsePartpath(value.partpath)
105
105
  if (!partpath) return null
106
- const kind = String(value.kind || '').trim()
106
+ const kind = String(value.kind || '')
107
107
  if (!kind) return null
108
108
  if (!Object.prototype.hasOwnProperty.call(value, 'query')) return null
109
109
  if (measureJsonBytes(value.query) > (tunables.maxQueryBytes ?? 2048)) return null
@@ -145,7 +145,7 @@ export function parsePartQueryRes(value, tunables = partQueryTunables) {
145
145
  */
146
146
  export function normalizePartQueryCacheMaterial(partpath, kind, query) {
147
147
  const path = parsePartpath(partpath)
148
- const k = String(kind || '').trim()
148
+ const k = String(kind || '')
149
149
  if (!path || !k) return null
150
150
  if (measureJsonBytes(query) > partQueryTunables.maxQueryBytes) return null
151
151
  try {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Buffer } from 'node:buffer'
5
5
 
6
- import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
6
+ import { isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
7
7
  import { pubKeyHash, verify } from '../crypto/crypto.mjs'
8
8
  import { eventBodyForSign, signPayloadBytes } from '../dag/index.mjs'
9
9
 
@@ -12,11 +12,11 @@ import { eventBodyForSign, signPayloadBytes } from '../dag/index.mjs'
12
12
  * @returns {Promise<boolean>} 验签是否通过
13
13
  */
14
14
  export async function verifyTimelineRemoteSignature(event) {
15
- const sender = String(event?.sender || '').trim().toLowerCase()
15
+ const sender = normalizeHex64(event?.sender)
16
16
  if (!isHex64(sender)) return false
17
- const signatureHex = String(event?.signature || '').trim()
17
+ const signatureHex = String(event?.signature || '')
18
18
  if (!isSignatureHex128(signatureHex)) return false
19
- const pubKeyHex = String(event?.senderPubKey || '').trim().toLowerCase()
19
+ const pubKeyHex = normalizeHex64(event?.senderPubKey)
20
20
  if (!isHex64(pubKeyHex)) return false
21
21
  const publicKeyBytes = new Uint8Array(Buffer.from(pubKeyHex, 'hex'))
22
22
  if (pubKeyHash(publicKeyBytes) !== sender) return false
@@ -30,7 +30,7 @@ export function createGroupLinkSet(options) {
30
30
  const allowNode = options.allowNode ?? (() => true)
31
31
  const { groupId } = options
32
32
  const scope = options.scope ?? `group:${groupId}`
33
- const roomSecret = options.roomSecret
33
+ const { roomSecret } = options
34
34
  const members = new Set(options.members || [])
35
35
  const selfNodeHash = registry.localIdentity.nodeHash
36
36
  const groupSettings = options.groupSettings ?? {}
@@ -59,7 +59,7 @@ export function createGroupLinkSet(options) {
59
59
  * @returns {void}
60
60
  */
61
61
  function registerCleanup(cleanup) {
62
- if (typeof cleanup !== 'function') return
62
+ if (!cleanup) return
63
63
  cleanups.add(cleanup)
64
64
  }
65
65
 
@@ -182,7 +182,7 @@ export function createGroupLinkSet(options) {
182
182
  }
183
183
  started = true
184
184
  active = true
185
- if (typeof registry.ensureRuntime === 'function')
185
+ if (registry.ensureRuntime)
186
186
  await registry.ensureRuntime()
187
187
  registry.registerScopeInterest(scope, [...members])
188
188
  registerCleanup(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
@@ -27,8 +27,8 @@ function normalizeIceEntry(raw) {
27
27
  if (!raw || typeof raw !== 'object') return null
28
28
  const urlsRaw = raw.urls
29
29
  const urlsList = Array.isArray(urlsRaw)
30
- ? urlsRaw.map(u => String(u).trim()).filter(Boolean)
31
- : [String(urlsRaw || '').trim()].filter(Boolean)
30
+ ? urlsRaw.map(u => String(u || '')).filter(Boolean)
31
+ : [String(urlsRaw || '')].filter(Boolean)
32
32
  if (!urlsList.length) return null
33
33
  for (const u of urlsList)
34
34
  if (!ICE_URL_RE.test(u)) return null
@@ -17,6 +17,7 @@ import { createLruMap } from '../utils/lru.mjs'
17
17
  import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
18
18
  import { createMeshKeepalive } from './mesh_keepalive.mjs'
19
19
  import { createOfferAnswerDial } from './offer_answer.mjs'
20
+ import { createPeerHealthTracker } from './peer_health.mjs'
20
21
  import { pickMeshEvictionVictim } from './peer_pool.mjs'
21
22
  import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
22
23
 
@@ -293,14 +294,14 @@ export function createLinkRegistry(options = {}) {
293
294
  const providers = listLinkProviders()
294
295
  for (const provider of providers)
295
296
  try {
296
- if (typeof provider.canReach === 'function') {
297
+ if (provider.canReach) {
297
298
  const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
298
299
  if (!reachable) {
299
300
  nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'canReach=false' })
300
301
  continue
301
302
  }
302
303
  }
303
- if (typeof provider.isAvailable === 'function') {
304
+ if (provider.isAvailable) {
304
305
  const available = await Promise.resolve(provider.isAvailable())
305
306
  if (!available) {
306
307
  nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'isAvailable=false' })
@@ -470,17 +471,6 @@ export function createLinkRegistry(options = {}) {
470
471
  || await relayEnvelopeToNode(remoteNodeHash, envelope)
471
472
  }
472
473
 
473
- /**
474
- * 同步结果直接返回;thenable 才 await(避免每条 envelope 造 microtask)。
475
- * @param {unknown} value 可能为 Promise 的返回值
476
- * @returns {Promise<unknown>} 已 resolve 的值
477
- */
478
- async function maybeAwait(value) {
479
- if (value != null && typeof /** @type {{ then?: unknown }} */ value.then === 'function')
480
- return await value
481
- return value
482
- }
483
-
484
474
  /**
485
475
  * 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
486
476
  * @param {string} senderNodeHash 发送方节点 64 hex
@@ -489,16 +479,15 @@ export function createLinkRegistry(options = {}) {
489
479
  * @returns {Promise<void>}
490
480
  */
491
481
  async function dispatchEnvelope(senderNodeHash, envelope, link) {
492
- const scope = envelope.scope || ''
482
+ const { scope } = envelope
493
483
  for (const [prefix, authorizer] of scopeAuthorizers.entries())
494
- if (scope.startsWith(prefix)) {
495
- const allowed = await maybeAwait(authorizer(scope, senderNodeHash, envelope, link))
496
- if (!allowed) return
497
- }
484
+ if (scope.startsWith(prefix))
485
+ if (!await authorizer(scope, senderNodeHash, envelope, link)) return
486
+
498
487
  for (const [prefix, listeners] of scopeListeners.entries())
499
488
  if (scope.startsWith(prefix))
500
489
  for (const listener of listeners)
501
- await maybeAwait(listener(senderNodeHash, envelope, link))
490
+ await listener(senderNodeHash, envelope, link)
502
491
  }
503
492
 
504
493
  /**
@@ -522,6 +511,25 @@ export function createLinkRegistry(options = {}) {
522
511
  return () => scopeAuthorizers.delete(prefix)
523
512
  }
524
513
 
514
+ const peerHealth = createPeerHealthTracker({
515
+ /**
516
+ * @param {(nodeHash: string, link: unknown) => void} listener link up 回调
517
+ * @returns {() => void} 取消订阅
518
+ */
519
+ onLinkUp: listener => {
520
+ linkUpListeners.add(listener)
521
+ return () => linkUpListeners.delete(listener)
522
+ },
523
+ /**
524
+ * @param {(nodeHash: string) => void} listener link down 回调
525
+ * @returns {() => void} 取消订阅
526
+ */
527
+ onLinkDown: listener => {
528
+ linkDownListeners.add(listener)
529
+ return () => linkDownListeners.delete(listener)
530
+ },
531
+ })
532
+
525
533
  return {
526
534
  localIdentity,
527
535
  buildLocalAdvert: bootstrap.buildLocalAdvert,
@@ -567,7 +575,7 @@ export function createLinkRegistry(options = {}) {
567
575
  * @returns {void}
568
576
  */
569
577
  setPriorityWeightFunction(weightFunction) {
570
- priorityWeightFunction = typeof weightFunction === 'function' ? weightFunction : null
578
+ priorityWeightFunction = weightFunction || null
571
579
  },
572
580
  /**
573
581
  * @param {string} nodeHash - 节点 64-hex hash
@@ -641,6 +649,22 @@ export function createLinkRegistry(options = {}) {
641
649
  },
642
650
  registerScopeAuthorizer,
643
651
  subscribeScope,
652
+ /**
653
+ * 获取指定邻居的健康记录。
654
+ * @param {string} nodeHash 节点 64 hex
655
+ * @returns {import('./peer_health.mjs').PeerHealthEntry | null} 健康记录;无记录时 null
656
+ */
657
+ getPeerHealth: peerHealth.getPeerHealth,
658
+ /**
659
+ * @returns {import('./peer_health.mjs').PeerHealthEntry[]} 所有邻居健康记录
660
+ */
661
+ listPeerHealth: peerHealth.listPeerHealth,
662
+ /**
663
+ * 订阅邻居健康变化。
664
+ * @param {(nodeHash: string, entry: import('./peer_health.mjs').PeerHealthEntry) => void} listener 回调
665
+ * @returns {() => void} 取消订阅
666
+ */
667
+ onPeerHealth: peerHealth.onPeerHealth,
644
668
  /**
645
669
  * 监听指定节点的 advert(per-hash,无 topic)。
646
670
  * @param {string} nodeHash 目标节点 64 hex
@@ -664,6 +688,7 @@ export function createLinkRegistry(options = {}) {
664
688
  */
665
689
  async shutdown() {
666
690
  await meshKeepalive?.stop()
691
+ peerHealth.stop()
667
692
  setDiscoveryLinkDialer(null)
668
693
  setDiscoveryPeerClueListener(null)
669
694
  await bootstrap.shutdown()
@@ -756,6 +781,12 @@ export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
756
781
  export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
757
782
  /** @type {(...methodArguments: unknown[]) => unknown} */
758
783
  export const subscribeScope = bindRegistryMethod('subscribeScope')
784
+ /** @type {(...methodArguments: unknown[]) => unknown} */
785
+ export const getPeerHealth = bindRegistryMethod('getPeerHealth')
786
+ /** @type {(...methodArguments: unknown[]) => unknown} */
787
+ export const listPeerHealth = bindRegistryMethod('listPeerHealth')
788
+ /** @type {(...methodArguments: unknown[]) => unknown} */
789
+ export const onPeerHealth = bindRegistryMethod('onPeerHealth')
759
790
 
760
791
  /**
761
792
  * 注册默认 registry 的 scope authorizer。
@@ -21,7 +21,7 @@ const INTENTIONAL_CLOSE = new Set([
21
21
  * @returns {boolean} 是否为本机主动关链(不重拨)
22
22
  */
23
23
  export function isMeshIntentionalClose(reason) {
24
- return INTENTIONAL_CLOSE.has(String(reason || ''))
24
+ return INTENTIONAL_CLOSE.has(reason || '')
25
25
  }
26
26
 
27
27
  /**
@@ -103,7 +103,7 @@ export function getNodeScopeContext() {
103
103
  */
104
104
  export function ensureNodeScope(options = {}) {
105
105
  if (options.replicaUsername != null)
106
- nodeScopeContext.replicaUsername = String(options.replicaUsername)
106
+ nodeScopeContext.replicaUsername = options.replicaUsername
107
107
  if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
108
108
  nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
109
109
  const handlers = nodeActionHandlers.get(envelope.action)