@steve02081504/fount-p2p 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
package/link/frame.mjs CHANGED
@@ -1,90 +1,56 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
- /**
4
- * 二进制帧协议版本号。
5
- */
6
- export const FRAME_VERSION = 1
7
- /**
8
- * msgId 字段字节长度(128 位)。
9
- */
10
- export const FRAME_MSG_ID_BYTES = 16
11
- /**
12
- * 帧头字节长度:version(1) + msgId(16) + seq(4) + total(4)。
13
- */
14
- export const FRAME_HEADER_BYTES = 1 + FRAME_MSG_ID_BYTES + 4 + 4
15
- /**
16
- * 默认单帧最大 chunk 大小(15 KiB)。
17
- */
3
+ import { bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
4
+
5
+ /** frameId 字段字节长度(128 位)。 */
6
+ export const FRAME_ID_BYTES = 16
7
+ /** 帧头:frameId(16) + seq(4) + total(4)。 */
8
+ export const FRAME_HEADER_BYTES = FRAME_ID_BYTES + 4 + 4
9
+ /** 默认单帧最大 chunk 大小(15 KiB)。 */
18
10
  export const DEFAULT_MAX_FRAME_CHUNK_BYTES = 15 * 1024
19
- /**
20
- * 重组后消息最大字节数(8 MiB)。
21
- */
11
+ /** 重组后消息最大字节数(8 MiB)。 */
22
12
  export const DEFAULT_MAX_MESSAGE_BYTES = 8 * 1024 * 1024
23
- /**
24
- * 同时进行中的分片消息数量上限。
25
- */
13
+ /** 同时进行中的分片消息数量上限。 */
26
14
  export const DEFAULT_MAX_PARTIAL_MESSAGES = 32
27
- /**
28
- * 分片消息超时时间(毫秒)。
29
- */
15
+ /** 分片消息超时时间(毫秒)。 */
30
16
  export const DEFAULT_PARTIAL_TIMEOUT_MS = 30_000
31
17
 
32
18
  /**
33
- * 将输入规范化为 Uint8Array
34
- * @param {unknown} value 原始字节数据
35
- * @returns {Uint8Array} 字节视图
19
+ * @param {string | Uint8Array} frameId hex 或 16 字节
20
+ * @returns {Uint8Array} 规范化后的 16 字节 frameId
36
21
  */
37
- function normalizeBytes(value) {
38
- if (value instanceof Uint8Array) return value
39
- if (value instanceof ArrayBuffer) return new Uint8Array(value)
40
- if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
41
- throw new Error('p2p: frame bytes must be Uint8Array-compatible')
42
- }
43
-
44
- /**
45
- * msgId 规范化为 16 字节 Uint8Array。
46
- * @param {unknown} msgId hex 字符串或 16 字节 Uint8Array
47
- * @returns {Uint8Array} 16 字节 msgId
48
- */
49
- function normalizeMsgIdBytes(msgId) {
50
- if (msgId instanceof Uint8Array) {
51
- if (msgId.byteLength !== FRAME_MSG_ID_BYTES)
52
- throw new Error(`p2p: msgId must be ${FRAME_MSG_ID_BYTES} bytes`)
53
- return msgId
22
+ function normalizeFrameIdBytes(frameId) {
23
+ if (frameId instanceof Uint8Array) {
24
+ if (frameId.byteLength !== FRAME_ID_BYTES)
25
+ throw new Error(`p2p: frameId must be ${FRAME_ID_BYTES} bytes`)
26
+ return frameId
27
+ }
28
+ const text = frameId.trim().toLowerCase()
29
+ if (text.length !== FRAME_ID_BYTES * 2)
30
+ throw new Error('p2p: frameId must be 32 hex characters')
31
+ try {
32
+ return hexToBytes(text)
33
+ }
34
+ catch {
35
+ throw new Error('p2p: frameId must be 32 hex characters')
54
36
  }
55
- const text = String(msgId ?? '').trim().toLowerCase()
56
- if (!/^[\da-f]{32}$/u.test(text))
57
- throw new Error('p2p: msgId must be 32 hex characters')
58
- return Uint8Array.from(text.match(/../g).map(chunk => Number.parseInt(chunk, 16)))
59
- }
60
-
61
- /**
62
- * 将 msgId 字节转为 32 字符小写 hex。
63
- * @param {Uint8Array} bytes 16 字节 msgId
64
- * @returns {string} hex 字符串
65
- */
66
- export function msgIdBytesToHex(bytes) {
67
- return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
68
37
  }
69
38
 
70
- /**
71
- * 生成随机 msgId hex 字符串。
72
- * @returns {string} 32 字符 hex msgId
73
- */
74
- export function randomMsgIdHex() {
75
- return msgIdBytesToHex(randomBytes(FRAME_MSG_ID_BYTES))
39
+ /** @returns {string} 32 字符 hex frameId */
40
+ export function randomFrameIdHex() {
41
+ return bytesToHex(randomBytes(FRAME_ID_BYTES))
76
42
  }
77
43
 
78
44
  /**
79
- * 将消息体拆分为带帧头的二进制帧数组。
80
- * @param {string | Uint8Array} msgId 消息 ID
81
- * @param {Uint8Array | ArrayBuffer | ArrayBufferView} bytes 消息体字节
82
- * @param {number} [maxChunkBytes=DEFAULT_MAX_FRAME_CHUNK_BYTES] 单帧最大 chunk 大小
83
- * @returns {Uint8Array[]} 帧数组
45
+ * 将消息切成带帧头的分片。
46
+ * @param {string | Uint8Array} frameId 消息 id(hex 或 16 字节)
47
+ * @param {Uint8Array | ArrayBuffer | ArrayBufferView} bytes 消息体
48
+ * @param {number} [maxChunkBytes] 单片上限
49
+ * @returns {Uint8Array[]} 分片帧列表
84
50
  */
85
- export function encodeFrames(msgId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_CHUNK_BYTES) {
86
- const body = normalizeBytes(bytes)
87
- const idBytes = normalizeMsgIdBytes(msgId)
51
+ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_CHUNK_BYTES) {
52
+ const body = toBytes(bytes)
53
+ const idBytes = normalizeFrameIdBytes(frameId)
88
54
  const chunkBytes = Math.max(256, Math.min(DEFAULT_MAX_MESSAGE_BYTES, Number(maxChunkBytes) || DEFAULT_MAX_FRAME_CHUNK_BYTES))
89
55
  const total = Math.max(1, Math.ceil(body.byteLength / chunkBytes))
90
56
  /** @type {Uint8Array[]} */
@@ -94,11 +60,10 @@ export function encodeFrames(msgId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_CHU
94
60
  const end = Math.min(body.byteLength, start + chunkBytes)
95
61
  const chunk = body.subarray(start, end)
96
62
  const frame = new Uint8Array(FRAME_HEADER_BYTES + chunk.byteLength)
97
- frame[0] = FRAME_VERSION
98
- frame.set(idBytes, 1)
63
+ frame.set(idBytes, 0)
99
64
  const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
100
- view.setUint32(1 + FRAME_MSG_ID_BYTES, seq, false)
101
- view.setUint32(1 + FRAME_MSG_ID_BYTES + 4, total, false)
65
+ view.setUint32(FRAME_ID_BYTES, seq, false)
66
+ view.setUint32(FRAME_ID_BYTES + 4, total, false)
102
67
  frame.set(chunk, FRAME_HEADER_BYTES)
103
68
  frames.push(frame)
104
69
  }
@@ -106,26 +71,22 @@ export function encodeFrames(msgId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_CHU
106
71
  }
107
72
 
108
73
  /**
109
- * 解析单帧二进制数据。
110
- * @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧字节
111
- * @returns {{ version: number, msgId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
74
+ * 解析单帧头与 chunk。
75
+ * @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧
76
+ * @returns {{ frameId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
112
77
  */
113
78
  export function decodeFrame(frame) {
114
- const bytes = normalizeBytes(frame)
79
+ const bytes = toBytes(frame)
115
80
  if (bytes.byteLength < FRAME_HEADER_BYTES)
116
81
  throw new Error('p2p: frame too short')
117
- const version = bytes[0]
118
- if (version !== FRAME_VERSION)
119
- throw new Error(`p2p: unsupported frame version ${version}`)
120
- const msgId = msgIdBytesToHex(bytes.subarray(1, 1 + FRAME_MSG_ID_BYTES))
82
+ const frameId = bytesToHex(bytes.subarray(0, FRAME_ID_BYTES))
121
83
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
122
- const seq = view.getUint32(1 + FRAME_MSG_ID_BYTES, false)
123
- const total = view.getUint32(1 + FRAME_MSG_ID_BYTES + 4, false)
84
+ const seq = view.getUint32(FRAME_ID_BYTES, false)
85
+ const total = view.getUint32(FRAME_ID_BYTES + 4, false)
124
86
  if (!total || seq >= total)
125
87
  throw new Error('p2p: invalid frame sequence')
126
88
  return {
127
- version,
128
- msgId,
89
+ frameId,
129
90
  seq,
130
91
  total,
131
92
  chunk: bytes.subarray(FRAME_HEADER_BYTES),
@@ -133,9 +94,8 @@ export function decodeFrame(frame) {
133
94
  }
134
95
 
135
96
  /**
136
- * 按序拼接多个 chunk 为完整消息体。
137
- * @param {Uint8Array[]} chunks 已排序的 chunk 数组
138
- * @returns {Uint8Array} 拼接后的消息体
97
+ * @param {Uint8Array[]} chunks 有序分片
98
+ * @returns {Uint8Array} 拼接结果
139
99
  */
140
100
  function concatChunks(chunks) {
141
101
  const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
@@ -149,96 +109,81 @@ function concatChunks(chunks) {
149
109
  }
150
110
 
151
111
  /**
152
- * 创建分片消息重组器。
153
- * @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [options] 大小、并发分片数与超时配置
154
- * @returns {{ push: (frame: Uint8Array | ArrayBuffer | ArrayBufferView, now?: number) => Uint8Array | null, prune: (now?: number) => string[], clear: () => void, size: () => number }} 重组器 API
112
+ * 创建分片重组器(按 frameId 聚合,超时 prune)。
113
+ * @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [options] 上限与超时
114
+ * @returns {{ push: (frame: Uint8Array | ArrayBuffer | ArrayBufferView, now?: number) => Uint8Array | null, prune: (now?: number) => string[], clear: () => void, size: () => number }} 重组 API
155
115
  */
156
116
  export function createReassembler(options = {}) {
157
117
  const maxMessageBytes = Math.max(1024, Number(options.maxMessageBytes) || DEFAULT_MAX_MESSAGE_BYTES)
158
118
  const maxPartials = Math.max(1, Number(options.maxPartials) || DEFAULT_MAX_PARTIAL_MESSAGES)
159
119
  const partialTimeoutMs = Math.max(1000, Number(options.partialTimeoutMs) || DEFAULT_PARTIAL_TIMEOUT_MS)
160
- /** @type {Map<string, { total: number, chunks: Uint8Array[], seen: boolean[], bytes: number, firstSeenAt: number, lastSeenAt: number }>} */
120
+ /** @type {Map<string, { total: number, remaining: number, chunks: Uint8Array[], bytes: number, firstSeenAt: number, lastSeenAt: number }>} */
161
121
  const partials = new Map()
162
122
 
163
- /**
164
- * 丢弃指定 msgId 的分片状态。
165
- * @param {string} msgId 消息 ID
166
- * @returns {void}
167
- */
168
- function drop(msgId) {
169
- partials.delete(msgId)
170
- }
171
-
172
123
  return {
173
124
  /**
174
- * 喂入一帧,收齐全部分片时返回完整消息体。
175
- * @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧字节
176
- * @param {number} [now=Date.now()] 当前时间戳(毫秒)
177
- * @returns {Uint8Array | null} 完整消息体,未收齐时返回 null
125
+ * 喂入一帧;凑齐则返回完整消息,否则 null。
126
+ * @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧
127
+ * @param {number} [now] 当前时间戳(测试可注入)
128
+ * @returns {Uint8Array | null} 完整消息或尚未齐
178
129
  */
179
130
  push(frame, now = Date.now()) {
180
131
  const parsed = decodeFrame(frame)
181
- if (!partials.has(parsed.msgId) && partials.size >= maxPartials)
132
+ if (!partials.has(parsed.frameId) && partials.size >= maxPartials)
182
133
  throw new Error('p2p: too many partial messages')
183
- let partial = partials.get(parsed.msgId)
134
+ let partial = partials.get(parsed.frameId)
184
135
  if (!partial) {
185
136
  partial = {
186
137
  total: parsed.total,
138
+ remaining: parsed.total,
187
139
  chunks: new Array(parsed.total),
188
- seen: new Array(parsed.total).fill(false),
189
140
  bytes: 0,
190
141
  firstSeenAt: now,
191
142
  lastSeenAt: now,
192
143
  }
193
- partials.set(parsed.msgId, partial)
144
+ partials.set(parsed.frameId, partial)
194
145
  }
195
146
  if (partial.total !== parsed.total) {
196
- drop(parsed.msgId)
147
+ partials.delete(parsed.frameId)
197
148
  throw new Error('p2p: frame total mismatch')
198
149
  }
199
150
  partial.lastSeenAt = now
200
- if (!partial.seen[parsed.seq]) {
151
+ if (!partial.chunks[parsed.seq]) {
201
152
  partial.chunks[parsed.seq] = parsed.chunk
202
- partial.seen[parsed.seq] = true
153
+ partial.remaining--
203
154
  partial.bytes += parsed.chunk.byteLength
204
155
  if (partial.bytes > maxMessageBytes) {
205
- drop(parsed.msgId)
156
+ partials.delete(parsed.frameId)
206
157
  throw new Error('p2p: reassembled message exceeds limit')
207
158
  }
208
159
  }
209
- if (partial.seen.every(Boolean)) {
160
+ if (partial.remaining === 0) {
210
161
  const out = concatChunks(partial.chunks)
211
- drop(parsed.msgId)
162
+ partials.delete(parsed.frameId)
212
163
  return out
213
164
  }
214
165
  return null
215
166
  },
216
167
  /**
217
- * 清理超时的分片消息。
218
- * @param {number} [now=Date.now()] 当前时间戳(毫秒)
219
- * @returns {string[]} 被丢弃的 msgId 列表
168
+ * 丢弃超时未齐的分片。
169
+ * @param {number} [now] 当前时间戳
170
+ * @returns {string[]} 被丢弃的 frameId 列表
220
171
  */
221
172
  prune(now = Date.now()) {
222
173
  /** @type {string[]} */
223
174
  const expired = []
224
- for (const [msgId, partial] of partials.entries())
175
+ for (const [frameId, partial] of partials)
225
176
  if (now - partial.lastSeenAt > partialTimeoutMs) {
226
- expired.push(msgId)
227
- partials.delete(msgId)
177
+ expired.push(frameId)
178
+ partials.delete(frameId)
228
179
  }
229
180
  return expired
230
181
  },
231
- /**
232
- * 清空所有进行中的分片状态。
233
- * @returns {void}
234
- */
182
+ /** @returns {void} */
235
183
  clear() {
236
184
  partials.clear()
237
185
  },
238
- /**
239
- * 返回当前进行中的分片消息数量。
240
- * @returns {number} 分片 msgId 数量
241
- */
186
+ /** @returns {number} 进行中的分片消息数 */
242
187
  size() {
243
188
  return partials.size
244
189
  },
@@ -51,7 +51,7 @@ export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
51
51
  /**
52
52
  * 构造 link hello 握手包。
53
53
  * @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
54
- * @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
54
+ * @returns {{ nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
55
55
  */
56
56
  export function buildHello(options = {}) {
57
57
  let publicKey = null
@@ -66,7 +66,7 @@ export function buildHello(options = {}) {
66
66
  throw new Error('p2p: invalid hello fields')
67
67
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
68
68
  throw new Error('p2p: hello nodePubKey does not match nodeHash')
69
- return { v: 1, nodeHash, nodePubKey, nonce }
69
+ return { nodeHash, nodePubKey, nonce }
70
70
  }
71
71
 
72
72
  /**
@@ -92,13 +92,12 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
92
92
  /**
93
93
  * 解析并校验 hello 对象,无效时返回 null。
94
94
  * @param {unknown} hello 原始 hello 载荷
95
- * @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
95
+ * @returns {{ nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
96
96
  */
97
97
  export function parseHello(hello) {
98
98
  const nodeHash = normalizeHex64(hello?.nodeHash)
99
99
  const nodePubKey = normalizeHex64(hello?.nodePubKey)
100
100
  const nonce = normalizeHex64(hello?.nonce)
101
- if (Number(hello?.v) !== 1) return null
102
101
  if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce)) return null
103
102
  try {
104
103
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash) return null
@@ -106,7 +105,7 @@ export function parseHello(hello) {
106
105
  catch {
107
106
  return null
108
107
  }
109
- return { v: 1, nodeHash, nodePubKey, nonce }
108
+ return { nodeHash, nodePubKey, nonce }
110
109
  }
111
110
 
112
111
  /**
@@ -136,26 +135,26 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
136
135
 
137
136
  /**
138
137
  * 构造 discovery advert 待签名字节串。
139
- * @param {string} topic 广播主题
138
+ * @param {string} rendezvousKey discovery 内部汇合键
140
139
  * @param {number} ts 时间戳(毫秒)
141
140
  * @param {string} nodeHash 节点 nodeHash
142
141
  * @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
143
142
  * @returns {Uint8Array} 待签名消息字节
144
143
  */
145
- export function buildAdvertMessage(topic, ts, nodeHash, tcpPort = null) {
146
- const base = `fount-advert\0${String(topic)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
144
+ export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null) {
145
+ const base = `fount-advert\0${String(rendezvousKey)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
147
146
  const port = normalizeTcpPort(tcpPort)
148
- return Buffer.from(port == null ? base : `${base}\0${port}`, 'utf8')
147
+ return Buffer.from(port ? `${base}\0${port}` : base, 'utf8')
149
148
  }
150
149
 
151
150
  /**
152
151
  * 构造带签名的 discovery advert。
153
- * @param {string} topic 广播主题
152
+ * @param {string} rendezvousKey discovery 内部汇合键
154
153
  * @param {number} [ts=Date.now()] 时间戳(毫秒)
155
154
  * @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number } | null} [options] 签名身份与可选 tcpPort
156
155
  * @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
157
156
  */
158
- export async function buildSignedAdvert(topic, ts = Date.now(), options = null) {
157
+ export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options = null) {
159
158
  const seed = options?.secretKey
160
159
  ? Buffer.from(options.secretKey)
161
160
  : Buffer.from(ensureNodeSeed(), 'hex')
@@ -165,9 +164,9 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
165
164
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
166
165
  throw new Error('p2p: advert nodePubKey does not match nodeHash')
167
166
  const tcpPort = normalizeTcpPort(options?.tcpPort)
168
- if (options?.tcpPort != null && options.tcpPort !== '' && tcpPort == null)
167
+ if (options?.tcpPort && !tcpPort)
169
168
  throw new Error('p2p: advert tcpPort invalid')
170
- const message = buildAdvertMessage(topic, ts, nodeHash, tcpPort)
169
+ const message = buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort)
171
170
  const sig = await sign(message, secretKey)
172
171
  const advert = {
173
172
  nodeHash,
@@ -175,28 +174,28 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
175
174
  ts,
176
175
  sig: Buffer.from(sig).toString('hex'),
177
176
  }
178
- if (tcpPort != null) advert.tcpPort = tcpPort
177
+ if (tcpPort) advert.tcpPort = tcpPort
179
178
  return advert
180
179
  }
181
180
 
182
181
  /**
183
182
  * 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash。
184
- * @param {string} topic 期望的广播主题
183
+ * @param {string} rendezvousKey 期望的汇合键
185
184
  * @param {unknown} advert 原始 advert 载荷
186
185
  * @param {number} [now=Date.now()] 当前时间(毫秒)
187
186
  * @param {number} [maxSkewMs=10 * 60_000] 允许的最大时钟偏差(毫秒)
188
187
  * @returns {Promise<string | null>} 验证通过的 nodeHash,失败返回 null
189
188
  */
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) })
189
+ export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
190
+ const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
192
191
  if (!parsedHello) return null
193
192
  const ts = Number(advert?.ts)
194
193
  const sig = String(advert?.sig ?? '').trim().toLowerCase()
195
194
  if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !/^[\da-f]{128}$/u.test(sig)) return null
196
- const hasTcpPortField = advert?.tcpPort != null && advert.tcpPort !== ''
195
+ const hasTcpPortField = !!advert?.tcpPort
197
196
  const tcpPort = normalizeTcpPort(advert?.tcpPort)
198
- if (hasTcpPortField && tcpPort == null) return null
199
- const message = buildAdvertMessage(topic, ts, parsedHello.nodeHash, tcpPort)
197
+ if (hasTcpPortField && !tcpPort) return null
198
+ const message = buildAdvertMessage(rendezvousKey, ts, parsedHello.nodeHash, tcpPort)
200
199
  const ok = await verify(Buffer.from(sig, 'hex'), message, Buffer.from(parsedHello.nodePubKey, 'hex'))
201
200
  return ok ? parsedHello.nodeHash : null
202
201
  }
package/link/pipe.mjs CHANGED
@@ -1,46 +1,20 @@
1
+ import { toBytes } from '../core/bytes_codec.mjs'
1
2
  import { normalizeHex64 } from '../core/hexIds.mjs'
2
3
  import { ms } from '../utils/duration.mjs'
4
+ import { emitSafe } from '../utils/emit_safe.mjs'
3
5
  import { createLruMap } from '../utils/lru.mjs'
4
6
 
5
- import { createReassembler, encodeFrames, randomMsgIdHex } from './frame.mjs'
7
+ import { createReassembler, encodeFrames, randomFrameIdHex } from './frame.mjs'
6
8
  import { buildAuth, buildHello, parseHello, verifyAuth } from './handshake.mjs'
7
9
 
8
10
  const encoder = new TextEncoder()
9
11
  const decoder = new TextDecoder()
10
12
 
11
- /**
12
- * 依次调用监听器集合,忽略单个 listener 抛错。
13
- * @param {Set<Function>} listeners 监听器集合
14
- * @param {...unknown} args 传递给 listener 的参数
15
- * @returns {void}
16
- */
17
- function emitListeners(listeners, ...args) {
18
- for (const listener of listeners)
19
- try { listener(...args) }
20
- catch { /* ignore */ }
21
- }
22
-
23
- /**
24
- * 原始字节:以 `{` 开头的 UTF-8 当 control 文本,否则当二进制帧。
25
- * @param {Buffer | Uint8Array | string} raw 原始数据
26
- * @returns {string | Uint8Array} control 文本或二进制帧
27
- */
28
- export function coercePipeInbound(raw) {
29
- if (typeof raw === 'string') return raw
30
- const bytes = raw instanceof Uint8Array ? raw : Uint8Array.from(raw)
31
- try {
32
- const text = decoder.decode(bytes)
33
- if (text.startsWith('{')) return text
34
- }
35
- catch { /* binary */ }
36
- return bytes
37
- }
38
-
39
13
  /**
40
14
  * 把 createLinkPipe 句柄收成上层 LinkHandle(可附带测试/内部字段)。
41
- * @param {ReturnType<typeof createLinkPipe>} pipe pipe
42
- * @param {object} [extras] 附加字段(如 handleInbound、_channelForTest)
43
- * @returns {object} LinkHandle 形状
15
+ * @param {ReturnType<typeof createLinkPipe>} pipe pipe 句柄
16
+ * @param {object} [extras] 额外字段(覆盖同名)
17
+ * @returns {object} LinkHandle
44
18
  */
45
19
  export function asLinkHandle(pipe, extras = {}) {
46
20
  return {
@@ -54,26 +28,26 @@ export function asLinkHandle(pipe, extras = {}) {
54
28
  /** @returns {number} 提供者 level */
55
29
  get level() { return pipe.level },
56
30
  /**
57
- * @param {...unknown} args send 参数
58
- * @returns {Promise<boolean>} 是否发送成功
31
+ * @param {...any} callArguments 透传 send
32
+ * @returns {ReturnType<typeof pipe.send>} 是否发送成功
59
33
  */
60
- send: (...args) => pipe.send(...args),
34
+ send: (...callArguments) => pipe.send(...callArguments),
61
35
  /**
62
- * @param {...unknown} args onEnvelope 参数
63
- * @returns {() => void} 取消订阅
36
+ * @param {...any} callArguments 透传 onEnvelope
37
+ * @returns {ReturnType<typeof pipe.onEnvelope>} 取消订阅
64
38
  */
65
- onEnvelope: (...args) => pipe.onEnvelope(...args),
39
+ onEnvelope: (...callArguments) => pipe.onEnvelope(...callArguments),
66
40
  /**
67
- * @param {...unknown} args onDown 参数
68
- * @returns {() => void} 取消订阅
41
+ * @param {...any} callArguments 透传 onDown
42
+ * @returns {ReturnType<typeof pipe.onDown>} 取消订阅
69
43
  */
70
- onDown: (...args) => pipe.onDown(...args),
44
+ onDown: (...callArguments) => pipe.onDown(...callArguments),
71
45
  /**
72
- * @param {...unknown} args close 参数
73
- * @returns {Promise<void>}
46
+ * @param {...any} callArguments 透传 close
47
+ * @returns {ReturnType<typeof pipe.close>} 关闭完成
74
48
  */
75
- close: (...args) => pipe.close(...args),
76
- /** @returns {object} 运行时统计 */
49
+ close: (...callArguments) => pipe.close(...callArguments),
50
+ /** @returns {ReturnType<typeof pipe.stats>} 运行时统计 */
77
51
  stats: () => pipe.stats(),
78
52
  ...extras,
79
53
  }
@@ -99,8 +73,7 @@ export function asLinkHandle(pipe, extras = {}) {
99
73
  * @returns {object} link 句柄 + 入站 API
100
74
  */
101
75
  export function createLinkPipe(options) {
102
- const providerId = String(options.providerId || '')
103
- const level = Number(options.level) || 0
76
+ const { providerId, level } = options
104
77
  const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
105
78
  const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
106
79
  const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
@@ -125,7 +98,7 @@ export function createLinkPipe(options) {
125
98
  let recvFrames = 0
126
99
  const envelopeListeners = new Set()
127
100
  const downListeners = new Set()
128
- const completedMsgIds = createLruMap(4096)
101
+ const completedFrameIds = createLruMap(4096)
129
102
  const reassembler = createReassembler()
130
103
  /** @type {(value: void | PromiseLike<void>) => void} */
131
104
  let resolveReady
@@ -219,9 +192,9 @@ export function createLinkPipe(options) {
219
192
  remoteHello = parsed
220
193
  await maybeSendAuth()
221
194
  if (pendingAuth) {
222
- const bufferedAuth = pendingAuth
195
+ const auth = pendingAuth
223
196
  pendingAuth = null
224
- await handleAuth(bufferedAuth)
197
+ await handleAuth(auth)
225
198
  }
226
199
  return
227
200
  }
@@ -240,9 +213,9 @@ export function createLinkPipe(options) {
240
213
  const merged = reassembler.push(bytes)
241
214
  if (!merged) return
242
215
  const envelope = JSON.parse(decoder.decode(merged))
243
- const msgId = envelope?.msgId ? String(envelope.msgId) : null
244
- if (msgId && completedMsgIds.has(msgId)) return
245
- if (msgId) completedMsgIds.touch(msgId, true)
216
+ const frameId = typeof envelope.frameId === 'string' ? envelope.frameId : null
217
+ if (frameId && completedFrameIds.has(frameId)) return
218
+ if (frameId) completedFrameIds.touch(frameId, true)
246
219
  if (envelope?.scope === 'link') {
247
220
  if (envelope.action === 'ping') {
248
221
  void send({ scope: 'link', action: 'pong', payload: {} }).catch(() => { })
@@ -251,7 +224,7 @@ export function createLinkPipe(options) {
251
224
  if (envelope.action === 'pong') return
252
225
  }
253
226
  if (ready && remoteNodeHash)
254
- emitListeners(envelopeListeners, envelope, remoteNodeHash)
227
+ emitSafe(envelopeListeners, envelope, remoteNodeHash)
255
228
  }
256
229
  catch {
257
230
  /* drop malformed network ingress */
@@ -278,23 +251,22 @@ export function createLinkPipe(options) {
278
251
  catch { /* ignore */ }
279
252
  return
280
253
  }
281
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Uint8Array) {
282
- const bytes = data instanceof Uint8Array
283
- ? data
284
- : data instanceof ArrayBuffer
285
- ? new Uint8Array(data)
286
- : new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
287
- // 尝试 UTF-8 JSON control
288
- try {
289
- const text = decoder.decode(bytes)
290
- if (text.startsWith('{')) {
291
- void handleControlMessage(JSON.parse(text))
292
- return
293
- }
254
+ let bytes
255
+ try {
256
+ bytes = toBytes(data)
257
+ }
258
+ catch {
259
+ return
260
+ }
261
+ try {
262
+ const text = decoder.decode(bytes)
263
+ if (text.startsWith('{')) {
264
+ void handleControlMessage(JSON.parse(text))
265
+ return
294
266
  }
295
- catch { /* binary path */ }
296
- handleBinaryFrame(bytes)
297
267
  }
268
+ catch { /* binary path */ }
269
+ handleBinaryFrame(bytes)
298
270
  }
299
271
 
300
272
  /**
@@ -318,15 +290,17 @@ export function createLinkPipe(options) {
318
290
  async function send(envelope) {
319
291
  await readyPromise
320
292
  if (closed) return false
321
- const message = {
322
- scope: String(envelope?.scope || ''),
323
- action: String(envelope?.action || ''),
324
- payload: envelope?.payload ?? null,
325
- msgId: randomMsgIdHex(),
326
- }
327
- const bytes = encoder.encode(JSON.stringify(message))
328
- for (const frame of encodeFrames(message.msgId, bytes)) {
329
- await Promise.resolve(options.sendFrame(message.action, frame))
293
+ const frameId = randomFrameIdHex()
294
+ const bytes = encoder.encode(JSON.stringify({
295
+ scope: envelope.scope,
296
+ action: envelope.action,
297
+ payload: envelope.payload ?? null,
298
+ frameId,
299
+ }))
300
+ for (const frame of encodeFrames(frameId, bytes)) {
301
+ const sent = options.sendFrame(envelope.action, frame)
302
+ if (sent != null && typeof /** @type {{ then?: unknown }} */ sent.then === 'function')
303
+ await sent
330
304
  sentFrames++
331
305
  }
332
306
  lastOutboundAt = Date.now()
@@ -347,7 +321,7 @@ export function createLinkPipe(options) {
347
321
  if (idleTimer) clearInterval(idleTimer)
348
322
  try { await Promise.resolve(options.closeTransport?.()) } catch { /* ignore */ }
349
323
  if (!ready) rejectReady(new Error(`p2p: link closed before ready (${reason})`))
350
- emitListeners(downListeners, reason)
324
+ emitSafe(downListeners, reason)
351
325
  }
352
326
 
353
327
  return {