@steve02081504/fount-p2p 0.0.12 → 0.0.14

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 (79) 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 +9 -3
  7. package/discovery/adverts.mjs +115 -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 +279 -0
  14. package/discovery/lan_interfaces.mjs +78 -0
  15. package/discovery/lan_peer_hints.mjs +22 -5
  16. package/discovery/nostr.mjs +474 -65
  17. package/federation/chunk_fetch_pending.mjs +10 -13
  18. package/federation/manifest_fetch_pending.mjs +1 -3
  19. package/files/assemble.mjs +14 -14
  20. package/files/assemble_stream.mjs +6 -6
  21. package/files/chunk_responder.mjs +29 -20
  22. package/files/evfs.mjs +29 -28
  23. package/files/manifest_fetch.mjs +16 -3
  24. package/files/public_manifest.mjs +11 -11
  25. package/files/transfer_key_registry.mjs +2 -2
  26. package/index.mjs +86 -11
  27. package/infra/cli.mjs +62 -0
  28. package/infra/debug_log.mjs +56 -0
  29. package/infra/default_node_dir.mjs +22 -0
  30. package/infra/priority.mjs +56 -0
  31. package/infra/service.mjs +140 -0
  32. package/infra/tunables.json +5 -0
  33. package/link/frame.mjs +9 -16
  34. package/link/handshake.mjs +25 -18
  35. package/link/pipe.mjs +8 -8
  36. package/link/providers/ble_gatt.mjs +6 -6
  37. package/link/providers/lan_tcp.mjs +62 -33
  38. package/link/providers/webrtc.mjs +3 -1
  39. package/link/rtc.mjs +3 -3
  40. package/mailbox/consumer_registry.mjs +2 -2
  41. package/mailbox/deliver_or_store.mjs +5 -5
  42. package/mailbox/wire.mjs +28 -24
  43. package/node/entity_store.mjs +6 -6
  44. package/node/instance.mjs +46 -27
  45. package/node/local_data_revision.mjs +26 -0
  46. package/node/log.mjs +56 -0
  47. package/node/network.mjs +37 -5
  48. package/node/reputation_store.mjs +4 -4
  49. package/node/reputation_sync.mjs +318 -0
  50. package/node/routing_profile.mjs +21 -0
  51. package/node/signaling_config.mjs +30 -11
  52. package/overlay/index.mjs +22 -1
  53. package/package.json +13 -2
  54. package/rooms/scoped_link.mjs +17 -166
  55. package/transport/advert_ingest.mjs +11 -14
  56. package/transport/group_link_set.mjs +149 -99
  57. package/transport/link_registry.mjs +211 -60
  58. package/transport/mesh_keepalive.mjs +217 -0
  59. package/transport/node_scope.mjs +289 -0
  60. package/transport/offer_answer.mjs +45 -48
  61. package/transport/peer_pool.mjs +116 -14
  62. package/transport/remote_user_room.mjs +6 -9
  63. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  64. package/transport/runtime_bootstrap.mjs +170 -108
  65. package/transport/tunables.json +11 -0
  66. package/transport/tunables.mjs +18 -0
  67. package/transport/user_room.mjs +83 -158
  68. package/trust_graph/build.mjs +0 -2
  69. package/trust_graph/cache.mjs +12 -3
  70. package/trust_graph/registry.mjs +6 -6
  71. package/trust_graph/send.mjs +0 -3
  72. package/utils/async_mutex.mjs +4 -4
  73. package/utils/emit_safe.mjs +3 -3
  74. package/utils/json_io.mjs +12 -12
  75. package/utils/map_pool.mjs +5 -5
  76. package/wire/part_ingress.mjs +32 -28
  77. package/wire/part_query.mjs +26 -20
  78. package/discovery/mdns.mjs +0 -197
  79. package/transport/signal_crypto.mjs +0 -104
@@ -0,0 +1,56 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { getReputationTable } from '../node/reputation_sync.mjs'
3
+ import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
4
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
5
+
6
+
7
+ /** @type {{ useLocalReputation: boolean }} */
8
+ let priorityConfig = { useLocalReputation: false }
9
+
10
+ const PRIORITY_BOOST = 1000
11
+
12
+ /**
13
+ * 配置 infra 路由加权(是否用本地 reputation)。
14
+ * @param {{ useLocalReputation?: boolean }} config - 是否用本地 reputation 加权路由
15
+ * @returns {void}
16
+ */
17
+ export function setInfraPriority(config = {}) {
18
+ priorityConfig = {
19
+ useLocalReputation: Boolean(config.useLocalReputation),
20
+ }
21
+ applyPriorityToRegistry()
22
+ }
23
+
24
+ /**
25
+ * @returns {{ useLocalReputation: boolean }} 当前 priority 配置副本
26
+ */
27
+ export function getInfraPriority() {
28
+ return { ...priorityConfig }
29
+ }
30
+
31
+ /**
32
+ * @returns {void}
33
+ */
34
+ export function applyPriorityToRegistry() {
35
+ const registry = getLinkRegistry()
36
+ if (!priorityConfig.useLocalReputation) {
37
+ registry.setPriorityWeightFunction(null)
38
+ return
39
+ }
40
+ registry.setPriorityWeightFunction(nodeHash => {
41
+ const score = pickNodeScoreFromReputation(
42
+ getReputationTable(),
43
+ normalizeHex64(nodeHash) || nodeHash,
44
+ )
45
+ return Math.floor(score * PRIORITY_BOOST)
46
+ })
47
+ }
48
+
49
+ /**
50
+ * stopInfra:卸 weight,并重置 priority 配置,避免再次 startInfra 幽灵恢复加权。
51
+ * @returns {void}
52
+ */
53
+ export function clearInfraPriorityFromRegistry() {
54
+ priorityConfig = { useLocalReputation: false }
55
+ getLinkRegistry().setPriorityWeightFunction(null)
56
+ }
@@ -0,0 +1,140 @@
1
+
2
+ import { isNodeInitialized } from '../node/instance.mjs'
3
+ import { setOverlayRateGate, clearOverlayRateGate } from '../overlay/index.mjs'
4
+ import { getLinkRegistry } from '../transport/link_registry.mjs'
5
+ import { attachNodeScopeMailbox } from '../transport/node_scope.mjs'
6
+
7
+ import { attachInfraDebugLog, detachInfraDebugLog } from './debug_log.mjs'
8
+ import {
9
+ applyPriorityToRegistry,
10
+ clearInfraPriorityFromRegistry,
11
+ getInfraPriority,
12
+ setInfraPriority,
13
+ } from './priority.mjs'
14
+ import infraTunables from './tunables.json' with { type: 'json' }
15
+
16
+ /** @type {Map<string, { tokens: number, updatedAt: number }>} */
17
+ const overlayRateBuckets = new Map()
18
+
19
+ /**
20
+ * Token bucket:桶容量 = burst,补充速率 = perMin/min。
21
+ * @param {Map<string, { tokens: number, updatedAt: number }>} buckets - 每 sender 桶状态
22
+ * @param {string} sender - 发送方 nodeHash
23
+ * @param {number} now - 当前时间戳(ms)
24
+ * @param {{ perMin: number, burst: number }} limits - 限速参数
25
+ * @returns {boolean} 是否允许本次 overlay 动作
26
+ */
27
+ export function consumeOverlayRateToken(buckets, sender, now, limits) {
28
+ const perMin = Math.max(1, limits.perMin)
29
+ const burst = Math.max(1, limits.burst)
30
+ const refillPerMs = perMin / 60_000
31
+ let bucket = buckets.get(sender)
32
+ if (!bucket) bucket = { tokens: burst, updatedAt: now }
33
+ const elapsed = Math.max(0, now - bucket.updatedAt)
34
+ bucket.tokens = Math.min(burst, bucket.tokens + elapsed * refillPerMs)
35
+ bucket.updatedAt = now
36
+ if (bucket.tokens < 1) {
37
+ buckets.set(sender, bucket)
38
+ return false
39
+ }
40
+ bucket.tokens -= 1
41
+ buckets.set(sender, bucket)
42
+ return true
43
+ }
44
+
45
+ /**
46
+ * @returns {void}
47
+ */
48
+ function installOverlayRateLimit() {
49
+ const limits = {
50
+ perMin: Math.max(1, Number(infraTunables.overlayRatePerMin) || 120),
51
+ burst: Math.max(1, Number(infraTunables.overlayRateBurst) || 30),
52
+ }
53
+ setOverlayRateGate((sender, action) => {
54
+ if (action !== 'route_req' && action !== 'relay') return true
55
+ return consumeOverlayRateToken(overlayRateBuckets, sender, Date.now(), limits)
56
+ })
57
+ }
58
+
59
+ /**
60
+ * @returns {void}
61
+ */
62
+ function removeOverlayRateLimit() {
63
+ clearOverlayRateGate()
64
+ overlayRateBuckets.clear()
65
+ }
66
+
67
+ /** @type {boolean} */
68
+ let infraRunning = false
69
+
70
+ /** @type {(() => void) | null} */
71
+ let mailboxDispose = null
72
+
73
+ /** @type {(() => void) | null} */
74
+ let debugDispose = null
75
+
76
+ /** @type {number | null} */
77
+ let savedMaxActive = null
78
+
79
+ /**
80
+ * @returns {boolean} infra relay 是否在运行
81
+ */
82
+ export function isInfraRunning() {
83
+ return infraRunning
84
+ }
85
+
86
+ /**
87
+ * 启动 public-good infra:overlay、mailbox、rate limit、priority、debug log。
88
+ * @param {{ maxActive?: number, logger?: { info?: Function, warn?: Function, error?: Function, log?: Function } | null }} [options] - maxActive 与 debug logger
89
+ * @returns {Promise<void>}
90
+ */
91
+ export async function startInfra(options = {}) {
92
+ if (!isNodeInitialized()) throw new Error('p2p: startInfra requires initNode')
93
+ if (infraRunning) {
94
+ if (options.maxActive != null) await getLinkRegistry().setMaxActive(options.maxActive)
95
+ if (options.logger !== undefined) {
96
+ detachInfraDebugLog()
97
+ debugDispose = attachInfraDebugLog(options.logger ?? null)
98
+ }
99
+ applyPriorityToRegistry()
100
+ return
101
+ }
102
+ const registry = getLinkRegistry()
103
+ await registry.ensureRuntime()
104
+ registry.ensureOverlayRouter()
105
+ installOverlayRateLimit()
106
+ savedMaxActive = registry.getMaxActive()
107
+ if (options.maxActive != null)
108
+ await registry.setMaxActive(options.maxActive)
109
+ else
110
+ await registry.setMaxActive(infraTunables.defaultMaxActive ?? savedMaxActive)
111
+ if (!mailboxDispose) mailboxDispose = attachNodeScopeMailbox()
112
+ const logger = options.logger === undefined ? console : options.logger ?? null
113
+ debugDispose = attachInfraDebugLog(logger)
114
+ applyPriorityToRegistry()
115
+ infraRunning = true
116
+ }
117
+
118
+ /**
119
+ * 卸掉 infra 自己挂的面(mailbox / rate / debug / priority / maxActive 恢复)。
120
+ * 不碰用户另行 attach 的 wires,不绑信誉同步。
121
+ * @returns {Promise<void>}
122
+ */
123
+ export async function stopInfra() {
124
+ if (!infraRunning) return
125
+ debugDispose?.()
126
+ debugDispose = null
127
+ detachInfraDebugLog()
128
+ clearInfraPriorityFromRegistry()
129
+ removeOverlayRateLimit()
130
+ mailboxDispose?.()
131
+ mailboxDispose = null
132
+ const registry = getLinkRegistry()
133
+ if (savedMaxActive != null)
134
+ await registry.setMaxActive(savedMaxActive)
135
+ savedMaxActive = null
136
+ infraRunning = false
137
+ }
138
+
139
+ /** 再导出:infra 路由加权配置(见 `priority.mjs`)。 */
140
+ export { getInfraPriority, setInfraPriority }
@@ -0,0 +1,5 @@
1
+ {
2
+ "defaultMaxActive": 96,
3
+ "overlayRatePerMin": 120,
4
+ "overlayRateBurst": 30
5
+ }
package/link/frame.mjs CHANGED
@@ -2,12 +2,10 @@ import { randomBytes } from 'node:crypto'
2
2
 
3
3
  import { bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
4
4
 
5
- /** 二进制帧协议版本号。 */
6
- export const FRAME_VERSION = 1
7
5
  /** frameId 字段字节长度(128 位)。 */
8
6
  export const FRAME_ID_BYTES = 16
9
- /** 帧头:version(1) + frameId(16) + seq(4) + total(4)。 */
10
- export const FRAME_HEADER_BYTES = 1 + FRAME_ID_BYTES + 4 + 4
7
+ /** 帧头:frameId(16) + seq(4) + total(4)。 */
8
+ export const FRAME_HEADER_BYTES = FRAME_ID_BYTES + 4 + 4
11
9
  /** 默认单帧最大 chunk 大小(15 KiB)。 */
12
10
  export const DEFAULT_MAX_FRAME_CHUNK_BYTES = 15 * 1024
13
11
  /** 重组后消息最大字节数(8 MiB)。 */
@@ -62,11 +60,10 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
62
60
  const end = Math.min(body.byteLength, start + chunkBytes)
63
61
  const chunk = body.subarray(start, end)
64
62
  const frame = new Uint8Array(FRAME_HEADER_BYTES + chunk.byteLength)
65
- frame[0] = FRAME_VERSION
66
- frame.set(idBytes, 1)
63
+ frame.set(idBytes, 0)
67
64
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
68
- view.setUint32(1 + FRAME_ID_BYTES, seq, false)
69
- view.setUint32(1 + FRAME_ID_BYTES + 4, total, false)
65
+ view.setUint32(FRAME_ID_BYTES, seq, false)
66
+ view.setUint32(FRAME_ID_BYTES + 4, total, false)
70
67
  frame.set(chunk, FRAME_HEADER_BYTES)
71
68
  frames.push(frame)
72
69
  }
@@ -76,23 +73,19 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
76
73
  /**
77
74
  * 解析单帧头与 chunk。
78
75
  * @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧
79
- * @returns {{ version: number, frameId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
76
+ * @returns {{ frameId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
80
77
  */
81
78
  export function decodeFrame(frame) {
82
79
  const bytes = toBytes(frame)
83
80
  if (bytes.byteLength < FRAME_HEADER_BYTES)
84
81
  throw new Error('p2p: frame too short')
85
- const version = bytes[0]
86
- if (version !== FRAME_VERSION)
87
- throw new Error(`p2p: unsupported frame version ${version}`)
88
- const frameId = bytesToHex(bytes.subarray(1, 1 + FRAME_ID_BYTES))
82
+ const frameId = bytesToHex(bytes.subarray(0, FRAME_ID_BYTES))
89
83
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
90
- const seq = view.getUint32(1 + FRAME_ID_BYTES, false)
91
- const total = view.getUint32(1 + FRAME_ID_BYTES + 4, false)
84
+ const seq = view.getUint32(FRAME_ID_BYTES, false)
85
+ const total = view.getUint32(FRAME_ID_BYTES + 4, false)
92
86
  if (!total || seq >= total)
93
87
  throw new Error('p2p: invalid frame sequence')
94
88
  return {
95
- version,
96
89
  frameId,
97
90
  seq,
98
91
  total,
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'
2
2
  import { randomBytes } from 'node:crypto'
3
3
 
4
4
  import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
5
+ import { normalizeLanHosts } from '../discovery/lan_interfaces.mjs'
5
6
  import { normalizeTcpPort } from '../core/tcp_port.mjs'
6
7
  import { keyPairFromSeed, pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
7
8
  import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
@@ -51,7 +52,7 @@ export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
51
52
  /**
52
53
  * 构造 link hello 握手包。
53
54
  * @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
54
- * @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
55
+ * @returns {{ nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
55
56
  */
56
57
  export function buildHello(options = {}) {
57
58
  let publicKey = null
@@ -66,7 +67,7 @@ export function buildHello(options = {}) {
66
67
  throw new Error('p2p: invalid hello fields')
67
68
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
68
69
  throw new Error('p2p: hello nodePubKey does not match nodeHash')
69
- return { v: 1, nodeHash, nodePubKey, nonce }
70
+ return { nodeHash, nodePubKey, nonce }
70
71
  }
71
72
 
72
73
  /**
@@ -92,13 +93,12 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
92
93
  /**
93
94
  * 解析并校验 hello 对象,无效时返回 null。
94
95
  * @param {unknown} hello 原始 hello 载荷
95
- * @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
96
+ * @returns {{ nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
96
97
  */
97
98
  export function parseHello(hello) {
98
99
  const nodeHash = normalizeHex64(hello?.nodeHash)
99
100
  const nodePubKey = normalizeHex64(hello?.nodePubKey)
100
101
  const nonce = normalizeHex64(hello?.nonce)
101
- if (Number(hello?.v) !== 1) return null
102
102
  if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce)) return null
103
103
  try {
104
104
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash) return null
@@ -106,7 +106,7 @@ export function parseHello(hello) {
106
106
  catch {
107
107
  return null
108
108
  }
109
- return { v: 1, nodeHash, nodePubKey, nonce }
109
+ return { nodeHash, nodePubKey, nonce }
110
110
  }
111
111
 
112
112
  /**
@@ -136,26 +136,30 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
136
136
 
137
137
  /**
138
138
  * 构造 discovery advert 待签名字节串。
139
- * @param {string} topic 广播主题
139
+ * @param {string} rendezvousKey discovery 内部汇合键
140
140
  * @param {number} ts 时间戳(毫秒)
141
141
  * @param {string} nodeHash 节点 nodeHash
142
142
  * @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
143
+ * @param {unknown} [lanHosts=null] 可选 LAN IPv4 列表(签入消息)
143
144
  * @returns {Uint8Array} 待签名消息字节
144
145
  */
145
- export function buildAdvertMessage(topic, ts, nodeHash, tcpPort = null) {
146
- const base = `fount-advert\0${String(topic)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
146
+ export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null) {
147
+ const base = `fount-advert\0${String(rendezvousKey)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
147
148
  const port = normalizeTcpPort(tcpPort)
148
- return Buffer.from(port ? `${base}\0${port}` : base, 'utf8')
149
+ let message = port ? `${base}\0${port}` : base
150
+ const hosts = normalizeLanHosts(lanHosts)
151
+ if (hosts.length) message += `\0${hosts.join(',')}`
152
+ return Buffer.from(message, 'utf8')
149
153
  }
150
154
 
151
155
  /**
152
156
  * 构造带签名的 discovery advert。
153
- * @param {string} topic 广播主题
157
+ * @param {string} rendezvousKey discovery 内部汇合键
154
158
  * @param {number} [ts=Date.now()] 时间戳(毫秒)
155
- * @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number } | null} [options] 签名身份与可选 tcpPort
156
- * @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
159
+ * @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number, lanHosts?: unknown }} | null} [options] 签名身份与可选 tcpPort / lanHosts
160
+ * @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number, lanHosts?: string[] }>} 签名 advert
157
161
  */
158
- export async function buildSignedAdvert(topic, ts = Date.now(), options = null) {
162
+ export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options = null) {
159
163
  const seed = options?.secretKey
160
164
  ? Buffer.from(options.secretKey)
161
165
  : Buffer.from(ensureNodeSeed(), 'hex')
@@ -167,7 +171,8 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
167
171
  const tcpPort = normalizeTcpPort(options?.tcpPort)
168
172
  if (options?.tcpPort && !tcpPort)
169
173
  throw new Error('p2p: advert tcpPort invalid')
170
- const message = buildAdvertMessage(topic, ts, nodeHash, tcpPort)
174
+ const lanHosts = normalizeLanHosts(options?.lanHosts)
175
+ const message = buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort, lanHosts)
171
176
  const sig = await sign(message, secretKey)
172
177
  const advert = {
173
178
  nodeHash,
@@ -176,19 +181,20 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
176
181
  sig: Buffer.from(sig).toString('hex'),
177
182
  }
178
183
  if (tcpPort) advert.tcpPort = tcpPort
184
+ if (lanHosts.length) advert.lanHosts = lanHosts
179
185
  return advert
180
186
  }
181
187
 
182
188
  /**
183
189
  * 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash。
184
- * @param {string} topic 期望的广播主题
190
+ * @param {string} rendezvousKey 期望的汇合键
185
191
  * @param {unknown} advert 原始 advert 载荷
186
192
  * @param {number} [now=Date.now()] 当前时间(毫秒)
187
193
  * @param {number} [maxSkewMs=10 * 60_000] 允许的最大时钟偏差(毫秒)
188
194
  * @returns {Promise<string | null>} 验证通过的 nodeHash,失败返回 null
189
195
  */
190
- export async function verifySignedAdvert(topic, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
191
- const parsedHello = parseHello({ v: 1, nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
196
+ export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
197
+ const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
192
198
  if (!parsedHello) return null
193
199
  const ts = Number(advert?.ts)
194
200
  const sig = String(advert?.sig ?? '').trim().toLowerCase()
@@ -196,7 +202,8 @@ export async function verifySignedAdvert(topic, advert, now = Date.now(), maxSke
196
202
  const hasTcpPortField = !!advert?.tcpPort
197
203
  const tcpPort = normalizeTcpPort(advert?.tcpPort)
198
204
  if (hasTcpPortField && !tcpPort) return null
199
- const message = buildAdvertMessage(topic, ts, parsedHello.nodeHash, tcpPort)
205
+ const lanHosts = normalizeLanHosts(advert?.lanHosts)
206
+ const message = buildAdvertMessage(rendezvousKey, ts, parsedHello.nodeHash, tcpPort, lanHosts)
200
207
  const ok = await verify(Buffer.from(sig, 'hex'), message, Buffer.from(parsedHello.nodePubKey, 'hex'))
201
208
  return ok ? parsedHello.nodeHash : null
202
209
  }
package/link/pipe.mjs CHANGED
@@ -28,25 +28,25 @@ export function asLinkHandle(pipe, extras = {}) {
28
28
  /** @returns {number} 提供者 level */
29
29
  get level() { return pipe.level },
30
30
  /**
31
- * @param {...any} args 透传 send
31
+ * @param {...any} callArguments 透传 send
32
32
  * @returns {ReturnType<typeof pipe.send>} 是否发送成功
33
33
  */
34
- send: (...args) => pipe.send(...args),
34
+ send: (...callArguments) => pipe.send(...callArguments),
35
35
  /**
36
- * @param {...any} args 透传 onEnvelope
36
+ * @param {...any} callArguments 透传 onEnvelope
37
37
  * @returns {ReturnType<typeof pipe.onEnvelope>} 取消订阅
38
38
  */
39
- onEnvelope: (...args) => pipe.onEnvelope(...args),
39
+ onEnvelope: (...callArguments) => pipe.onEnvelope(...callArguments),
40
40
  /**
41
- * @param {...any} args 透传 onDown
41
+ * @param {...any} callArguments 透传 onDown
42
42
  * @returns {ReturnType<typeof pipe.onDown>} 取消订阅
43
43
  */
44
- onDown: (...args) => pipe.onDown(...args),
44
+ onDown: (...callArguments) => pipe.onDown(...callArguments),
45
45
  /**
46
- * @param {...any} args 透传 close
46
+ * @param {...any} callArguments 透传 close
47
47
  * @returns {ReturnType<typeof pipe.close>} 关闭完成
48
48
  */
49
- close: (...args) => pipe.close(...args),
49
+ close: (...callArguments) => pipe.close(...callArguments),
50
50
  /** @returns {ReturnType<typeof pipe.stats>} 运行时统计 */
51
51
  stats: () => pipe.stats(),
52
52
  ...extras,
@@ -207,9 +207,9 @@ export function createBleGattLinkProvider() {
207
207
  * @returns {void}
208
208
  */
209
209
  onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
210
- const buf = Buffer.from(data)
211
- if (sessionInbound) sessionInbound(buf)
212
- else void acceptPeripheralWrite(buf).catch(() => { })
210
+ const buffer = Buffer.from(data)
211
+ if (sessionInbound) sessionInbound(buffer)
212
+ else void acceptPeripheralWrite(buffer).catch(() => { })
213
213
  callback(bleno.Characteristic.RESULT_SUCCESS)
214
214
  },
215
215
  })
@@ -228,12 +228,12 @@ export function createBleGattLinkProvider() {
228
228
  }
229
229
 
230
230
  /**
231
- * @param {Buffer} buf 入站写(首包应为 link-open)
231
+ * @param {Buffer} buffer 入站写(首包应为 link-open)
232
232
  * @returns {Promise<void>}
233
233
  */
234
- async function acceptPeripheralWrite(buf) {
234
+ async function acceptPeripheralWrite(buffer) {
235
235
  if (activeInbound || acceptInflight || !onInbound || !localIdentity) return
236
- const opened = parseLinkOpen(buf)
236
+ const opened = parseLinkOpen(buffer)
237
237
  if (!opened) return
238
238
  acceptInflight = true
239
239
  try {
@@ -3,13 +3,50 @@ import { randomBytes } from 'node:crypto'
3
3
  import net from 'node:net'
4
4
 
5
5
  import { normalizeHex64 } from '../../core/hexIds.mjs'
6
- import { getLanPeerHint } from '../../discovery/lan_peer_hints.mjs'
6
+ import { getLanPeerHint, listLanPeerHints } from '../../discovery/lan_peer_hints.mjs'
7
7
  import { asLinkHandle } from '../pipe.mjs'
8
8
 
9
9
  import { LINK_LEVEL_LAN_TCP } from './levels.mjs'
10
10
  import { buildLinkOpen, createLinkIdBoundPipe, parseLinkOpen } from './link_id_pipe.mjs'
11
11
 
12
12
  const MAX_FRAME_BYTES = 1 << 20
13
+ /** LAN TCP 单 endpoint 连接超时。 */
14
+ const LAN_TCP_CONNECT_TIMEOUT_MS = 3_000
15
+
16
+ /**
17
+ * @param {string} host 目标 host
18
+ * @param {number} port 目标 port
19
+ * @param {number} [timeoutMs=LAN_TCP_CONNECT_TIMEOUT_MS] 超时
20
+ * @returns {Promise<import('node:net').Socket>} 已连接 socket
21
+ */
22
+ function connectTcp(host, port, timeoutMs = LAN_TCP_CONNECT_TIMEOUT_MS) {
23
+ return new Promise((resolve, reject) => {
24
+ const conn = net.createConnection({ host, port })
25
+ const timer = setTimeout(() => {
26
+ conn.destroy()
27
+ reject(new Error(`p2p: lan_tcp connect timeout ${host}:${port}`))
28
+ }, timeoutMs)
29
+ /**
30
+ * @param {Error} error 连接错误
31
+ * @returns {void}
32
+ */
33
+ const onError = error => {
34
+ clearTimeout(timer)
35
+ conn.off('connect', onConnect)
36
+ reject(error)
37
+ }
38
+ /**
39
+ * @returns {void}
40
+ */
41
+ function onConnect() {
42
+ clearTimeout(timer)
43
+ conn.off('error', onError)
44
+ resolve(conn)
45
+ }
46
+ conn.once('error', onError)
47
+ conn.once('connect', onConnect)
48
+ })
49
+ }
13
50
 
14
51
  /**
15
52
  * 在 socket 上挂 length-prefix 编解码(u32be + payload)。
@@ -168,41 +205,33 @@ async function openTcpPipe(options) {
168
205
  */
169
206
  async function dialLanTcp(options) {
170
207
  const remoteNodeHash = normalizeHex64(options.nodeHash)
171
- const hint = getLanPeerHint(remoteNodeHash)
172
- if (!hint) throw new Error('p2p: lan_tcp no peer hint')
208
+ const hints = listLanPeerHints(remoteNodeHash)
209
+ if (!hints.length) throw new Error('p2p: lan_tcp no peer hint')
173
210
 
174
- const socket = await new Promise((resolve, reject) => {
175
- const conn = net.createConnection({ host: hint.host, port: hint.port })
176
- /**
177
- * @param {Error} error 连接错误
178
- * @returns {void}
179
- */
180
- const onError = error => {
181
- conn.off('connect', onConnect)
182
- reject(error)
211
+ let lastError = null
212
+ for (const hint of hints) {
213
+ /** @type {import('node:net').Socket | null} */
214
+ let socket = null
215
+ try {
216
+ socket = await connectTcp(hint.host, hint.port)
217
+ const link = await openTcpPipe({
218
+ initiator: true,
219
+ linkId: randomBytes(32).toString('hex'),
220
+ nodeHash: remoteNodeHash,
221
+ localIdentity: options.localIdentity,
222
+ socket,
223
+ host: hint.host,
224
+ port: hint.port,
225
+ })
226
+ await link.ready
227
+ return link
183
228
  }
184
- /**
185
- * @returns {void}
186
- */
187
- function onConnect() {
188
- conn.off('error', onError)
189
- resolve(conn)
229
+ catch (error) {
230
+ lastError = error
231
+ try { socket?.destroy() } catch { /* ignore */ }
190
232
  }
191
- conn.once('error', onError)
192
- conn.once('connect', onConnect)
193
- })
194
-
195
- const link = await openTcpPipe({
196
- initiator: true,
197
- linkId: randomBytes(32).toString('hex'),
198
- nodeHash: remoteNodeHash,
199
- localIdentity: options.localIdentity,
200
- socket,
201
- host: hint.host,
202
- port: hint.port,
203
- })
204
- await link.ready
205
- return link
233
+ }
234
+ throw lastError || new Error('p2p: lan_tcp dial failed')
206
235
  }
207
236
 
208
237
  /**
@@ -1,4 +1,5 @@
1
1
  import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
2
+ import { nodeDebug } from '../../node/log.mjs'
2
3
  import { ms } from '../../utils/duration.mjs'
3
4
  import { createLruMap } from '../../utils/lru.mjs'
4
5
  import {
@@ -43,8 +44,9 @@ export async function canUseWebRtcLink() {
43
44
  await loadNodeRtcPolyfill()
44
45
  cachedAvailable = true
45
46
  }
46
- catch {
47
+ catch (error) {
47
48
  cachedAvailable = false
49
+ nodeDebug('p2p:webrtc unavailable', { err: formatErrorReason(error) })
48
50
  }
49
51
  return cachedAvailable
50
52
  }
package/link/rtc.mjs CHANGED
@@ -2,7 +2,7 @@ import process from 'node:process'
2
2
 
3
3
  import { toBytes } from '../core/bytes_codec.mjs'
4
4
  import { getSignalingRuntimeConfig } from '../node/instance.mjs'
5
- import { wrapRtcPeerConnectionForMdns } from '../transport/rtc_mdns_filter.mjs'
5
+ import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
6
6
 
7
7
  /**
8
8
  * 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
@@ -20,9 +20,9 @@ process.on('exit', () => {
20
20
  */
21
21
  export async function loadNodeRtcPolyfill() {
22
22
  const mod = await import('node-datachannel/polyfill')
23
- const { mdnsPolicy } = getSignalingRuntimeConfig()
23
+ const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
24
24
  return {
25
- RTCPeerConnection: wrapRtcPeerConnectionForMdns(mod.RTCPeerConnection, mod.RTCIceCandidate, mdnsPolicy),
25
+ RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
26
26
  RTCIceCandidate: mod.RTCIceCandidate,
27
27
  }
28
28
  }
@@ -54,8 +54,8 @@ export async function dispatchMailboxRecordsToConsumers(username, records) {
54
54
  const ids = await handler(username, scoped)
55
55
  for (const id of ids || []) delivered.add(String(id))
56
56
  }
57
- catch (err) {
58
- console.error('mailbox: consumer batch failed, retry per record', err)
57
+ catch (error) {
58
+ console.error('mailbox: consumer batch failed, retry per record', error)
59
59
  for (const row of scoped)
60
60
  try {
61
61
  const ids = await handler(username, [row])
@@ -1,6 +1,6 @@
1
1
  import { normalizeHex64 } from '../core/hexIds.mjs'
2
2
  import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
3
- import { deliverToUserRoomPeers, ensureUserRoom } from '../transport/user_room.mjs'
3
+ import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
4
4
  import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
5
5
 
6
6
  import { allowMailboxRelayForTier } from './importance.mjs'
@@ -23,9 +23,9 @@ import {
23
23
  * @returns {Promise<string | null>} 已验证的 remote nodeHash
24
24
  */
25
25
  async function resolveRemoteNodeHashForPeer(username, peerId) {
26
+ void username
26
27
  if (!peerId) return null
27
- const slot = await ensureUserRoom({ replicaUsername: username })
28
- const entry = slot?.getRoster()?.find(row => row.peerId === peerId)
28
+ const entry = activeLinkRoster().find(row => row.peerId === peerId)
29
29
  const remote = entry?.remoteNodeHash?.trim().toLowerCase()
30
30
  return remote || null
31
31
  }
@@ -51,9 +51,9 @@ async function resolveRelayHopForIngress(record) {
51
51
  * @returns {Promise<{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }>} 按在线 peer 数缩放的路由
52
52
  */
53
53
  async function resolveRouting(username) {
54
+ void username
54
55
  const { batterySaver, mailbox } = getNodeTransportSettings()
55
- const slot = await ensureUserRoom({ replicaUsername: username })
56
- const peerCount = slot?.getRoster()?.length ?? 0
56
+ const peerCount = activeLinkRoster().length
57
57
  return resolveMailboxRoutingForPeerCount(peerCount, mailbox, batterySaver)
58
58
  }
59
59