@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
@@ -0,0 +1,318 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
4
+ import { sendToNodeLink } from '../transport/link_registry.mjs'
5
+ import { attachNodeScopeFeature, ensureNodeScope, getNodeScopeWire } from '../transport/node_scope.mjs'
6
+
7
+ import {
8
+ loadReputation,
9
+ mutateReputation,
10
+ } from './reputation_store.mjs'
11
+ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
12
+
13
+ const SYNC_DATA_NAME = 'reputation_sync'
14
+ const MAX_LOCKED_SCORE = 1
15
+
16
+ /**
17
+ * @typedef {{
18
+ * trustSyncDonors: string[]
19
+ * reputationExportAllowlist: string[]
20
+ * lockedMaxNodeHashes: string[]
21
+ * lockedMaxPrevByNodeHash: Record<string, number>
22
+ * }} ReputationSyncConfig
23
+ */
24
+
25
+ /** @type {ReputationSyncConfig | null} */
26
+ let syncConfig = null
27
+
28
+ /** @type {Map<string, { resolve: (v: object) => void, reject: (e: Error) => void, timer: ReturnType<typeof setTimeout>, donor: string }>} */
29
+ const pendingPulls = new Map()
30
+
31
+ /** @type {Set<() => void>} */
32
+ const syncWireDisposers = new Set()
33
+
34
+ let pullTimeoutMs = 8000
35
+
36
+ /**
37
+ * @param {number} ms 超时毫秒;测试用
38
+ * @returns {void}
39
+ */
40
+ export function setReputationPullTimeoutMsForTests(ms) {
41
+ pullTimeoutMs = Math.max(1, Number(ms) || 8000)
42
+ }
43
+
44
+ /**
45
+ * @returns {ReputationSyncConfig} 内存中的 sync 配置(首次从盘加载)
46
+ */
47
+ function loadSyncConfig() {
48
+ if (syncConfig) return syncConfig
49
+ const raw = readNodeJsonSync(SYNC_DATA_NAME) || {}
50
+ /** @type {Record<string, number>} */
51
+ const lockedMaxPrevByNodeHash = {}
52
+ const prevRaw = raw.lockedMaxPrevByNodeHash && typeof raw.lockedMaxPrevByNodeHash === 'object'
53
+ ? raw.lockedMaxPrevByNodeHash
54
+ : {}
55
+ for (const [nodeHash, score] of Object.entries(prevRaw)) {
56
+ const id = normalizeHex64(nodeHash)
57
+ const n = Number(score)
58
+ if (id && isHex64(id) && Number.isFinite(n)) lockedMaxPrevByNodeHash[id] = n
59
+ }
60
+ syncConfig = {
61
+ trustSyncDonors: normalizeHashList(raw.trustSyncDonors),
62
+ reputationExportAllowlist: normalizeHashList(raw.reputationExportAllowlist),
63
+ lockedMaxNodeHashes: normalizeHashList(raw.lockedMaxNodeHashes),
64
+ lockedMaxPrevByNodeHash,
65
+ }
66
+ return syncConfig
67
+ }
68
+
69
+ /**
70
+ * @param {unknown} list - 原始 hash 列表
71
+ * @returns {string[]} 规范化去重后的 64-hex 列表
72
+ */
73
+ function normalizeHashList(list) {
74
+ return [...new Set((Array.isArray(list) ? list : [])
75
+ .map(id => normalizeHex64(id))
76
+ .filter(id => isHex64(id)))]
77
+ }
78
+
79
+ /**
80
+ * @returns {void}
81
+ */
82
+ function persistSyncConfig() {
83
+ writeNodeJsonSync(SYNC_DATA_NAME, loadSyncConfig())
84
+ }
85
+
86
+ /**
87
+ * @returns {object} 当前 reputation 表(byNodeHash)
88
+ */
89
+ export function getReputationTable() {
90
+ return loadReputation()
91
+ }
92
+
93
+ /**
94
+ * @param {object} table - 含 byNodeHash 的信誉表或裸 byNodeHash 对象
95
+ * @returns {void}
96
+ */
97
+ export async function setReputationTable(table) {
98
+ const incoming = table?.byNodeHash && typeof table.byNodeHash === 'object' ? table.byNodeHash : table
99
+ if (!incoming || typeof incoming !== 'object') throw new Error('p2p: setReputationTable requires byNodeHash object')
100
+ await mutateReputation(data => {
101
+ data.byNodeHash = data.byNodeHash || {}
102
+ for (const [nodeHash, row] of Object.entries(incoming)) {
103
+ const id = normalizeHex64(nodeHash)
104
+ if (!id || !isHex64(id)) continue
105
+ const score = Number(row?.score ?? row)
106
+ if (!Number.isFinite(score)) continue
107
+ data.byNodeHash[id] = { ...data.byNodeHash[id] || {}, score }
108
+ }
109
+ applyLocksToReputation(data)
110
+ })
111
+ }
112
+
113
+ /**
114
+ * @param {object} data - reputation 存储对象
115
+ * @returns {void}
116
+ */
117
+ function applyLocksToReputation(data) {
118
+ for (const nodeHash of loadSyncConfig().lockedMaxNodeHashes)
119
+ if (!data.byNodeHash[nodeHash]) data.byNodeHash[nodeHash] = { score: MAX_LOCKED_SCORE }
120
+ else data.byNodeHash[nodeHash].score = MAX_LOCKED_SCORE
121
+ }
122
+
123
+ /**
124
+ * 将节点分数钳到上限;首次 lock 时记下原分,unlock 时还原。
125
+ * @param {string[]} nodeHashes - 要 lock 的节点 hash 列表
126
+ * @returns {Promise<void>}
127
+ */
128
+ export function lockReputationMax(nodeHashes) {
129
+ const config = loadSyncConfig()
130
+ const hashes = normalizeHashList(nodeHashes)
131
+ return mutateReputation(data => {
132
+ data.byNodeHash = data.byNodeHash || {}
133
+ for (const hash of hashes) {
134
+ if (config.lockedMaxNodeHashes.includes(hash)) continue
135
+ const prev = Number(data.byNodeHash[hash]?.score)
136
+ config.lockedMaxPrevByNodeHash[hash] = Number.isFinite(prev) ? prev : 0
137
+ config.lockedMaxNodeHashes.push(hash)
138
+ }
139
+ persistSyncConfig()
140
+ applyLocksToReputation(data)
141
+ })
142
+ }
143
+
144
+ /**
145
+ * 解除上限钳;还原 lock 前记下的分数。
146
+ * @param {string[]} nodeHashes - 要 unlock 的节点 hash 列表
147
+ * @returns {Promise<void>}
148
+ */
149
+ export function unlockReputationMax(nodeHashes) {
150
+ const config = loadSyncConfig()
151
+ const remove = new Set(normalizeHashList(nodeHashes))
152
+ if (!remove.size) return Promise.resolve()
153
+ config.lockedMaxNodeHashes = config.lockedMaxNodeHashes.filter(id => !remove.has(id))
154
+ /** @type {Record<string, number>} */
155
+ const restore = {}
156
+ for (const hash of remove)
157
+ if (Object.hasOwn(config.lockedMaxPrevByNodeHash, hash)) {
158
+ restore[hash] = config.lockedMaxPrevByNodeHash[hash]
159
+ delete config.lockedMaxPrevByNodeHash[hash]
160
+ }
161
+
162
+ persistSyncConfig()
163
+ return mutateReputation(data => {
164
+ data.byNodeHash = data.byNodeHash || {}
165
+ for (const [hash, score] of Object.entries(restore))
166
+ if (!data.byNodeHash[hash]) data.byNodeHash[hash] = { score }
167
+ else data.byNodeHash[hash].score = score
168
+ })
169
+ }
170
+
171
+ /**
172
+ * @returns {string[]} 当前锁定为满分的节点 hash 列表
173
+ */
174
+ export function getReputationLocks() {
175
+ return [...loadSyncConfig().lockedMaxNodeHashes]
176
+ }
177
+
178
+ /**
179
+ * @param {string[]} donors - 允许拉取信誉的 donor 节点
180
+ * @returns {void}
181
+ */
182
+ export function setTrustSyncDonors(donors) {
183
+ loadSyncConfig().trustSyncDonors = normalizeHashList(donors)
184
+ persistSyncConfig()
185
+ }
186
+
187
+ /**
188
+ * @returns {string[]} 当前 trustSyncDonors 副本
189
+ */
190
+ export function getTrustSyncDonors() {
191
+ return [...loadSyncConfig().trustSyncDonors]
192
+ }
193
+
194
+ /**
195
+ * @param {string[]} allowlist - 允许导出本机信誉表的节点
196
+ * @returns {void}
197
+ */
198
+ export function setReputationExportAllowlist(allowlist) {
199
+ loadSyncConfig().reputationExportAllowlist = normalizeHashList(allowlist)
200
+ persistSyncConfig()
201
+ }
202
+
203
+ /**
204
+ * @returns {string[]} 当前 reputationExportAllowlist 副本
205
+ */
206
+ export function getReputationExportAllowlist() {
207
+ return [...loadSyncConfig().reputationExportAllowlist]
208
+ }
209
+
210
+ /**
211
+ * @returns {object} 仅含 score 的导出表
212
+ */
213
+ function exportScoreTable() {
214
+ const rep = loadReputation()
215
+ /** @type {Record<string, { score: number }>} */
216
+ const byNodeHash = {}
217
+ for (const [nodeHash, row] of Object.entries(rep.byNodeHash || {}))
218
+ byNodeHash[nodeHash] = { score: Number(row?.score ?? 0) }
219
+ return { byNodeHash }
220
+ }
221
+
222
+ /**
223
+ * 挂载信誉同步 wire(refcount;处理 rep_sync_req / rep_sync_res)。
224
+ * @returns {() => void} 取消 wire 挂载的 dispose
225
+ */
226
+ export function attachReputationSyncWire() {
227
+ ensureNodeScope()
228
+ if (!getNodeScopeWire()) throw new Error('p2p: attachReputationSyncWire requires node scope wire')
229
+ const dispose = attachNodeScopeFeature('rep_sync', wire => {
230
+ const offs = [
231
+ wire.on('rep_sync_req', (payload, peerId) => {
232
+ const requester = normalizeHex64(peerId)
233
+ if (!requester || !getReputationExportAllowlist().includes(requester)) return
234
+ try {
235
+ wire.send('rep_sync_res', {
236
+ requestId: payload?.requestId,
237
+ ...exportScoreTable(),
238
+ }, peerId)
239
+ }
240
+ catch { /* disconnected */ }
241
+ }),
242
+ wire.on('rep_sync_res', (payload, peerId) => {
243
+ const requestId = String(payload?.requestId || '')
244
+ const pending = pendingPulls.get(requestId)
245
+ if (!pending) return
246
+ if (normalizeHex64(peerId) !== pending.donor) return
247
+ clearTimeout(pending.timer)
248
+ pendingPulls.delete(requestId)
249
+ pending.resolve(payload)
250
+ }),
251
+ ]
252
+ return () => {
253
+ for (const off of offs)
254
+ try { off?.() } catch { /* ignore */ }
255
+ }
256
+ })
257
+ syncWireDisposers.add(dispose)
258
+ return () => {
259
+ if (!syncWireDisposers.delete(dispose)) return
260
+ dispose()
261
+ }
262
+ }
263
+
264
+ /**
265
+ * 卸掉信誉同步 wire(强制清掉全部 ref)。
266
+ * @returns {void}
267
+ */
268
+ export function detachReputationSyncWire() {
269
+ for (const dispose of [...syncWireDisposers]) {
270
+ syncWireDisposers.delete(dispose)
271
+ try { dispose() } catch { /* ignore */ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * 从 donor 拉取信誉表 JSON;不落盘。应用需自行 `setReputationTable`。
277
+ * @param {string} nodeHash - donor 节点 64-hex hash
278
+ * @returns {Promise<object>} donor 返回的信誉表
279
+ */
280
+ export async function pullReputationFromNode(nodeHash) {
281
+ const donor = normalizeHex64(nodeHash)
282
+ if (!donor || !isHex64(donor)) throw new Error('p2p: pullReputationFromNode requires valid nodeHash')
283
+ if (!getTrustSyncDonors().includes(donor))
284
+ throw new Error('p2p: node not in trustSyncDonors')
285
+ attachReputationSyncWire()
286
+ const requestId = randomUUID()
287
+ const resultPromise = new Promise((resolve, reject) => {
288
+ const timer = setTimeout(() => {
289
+ pendingPulls.delete(requestId)
290
+ reject(new Error('p2p: rep_sync timeout'))
291
+ }, pullTimeoutMs)
292
+ pendingPulls.set(requestId, { resolve, reject, timer, donor })
293
+ })
294
+ const ok = await sendToNodeLink(donor, {
295
+ scope: 'node',
296
+ action: 'rep_sync_req',
297
+ payload: { requestId },
298
+ })
299
+ if (!ok) {
300
+ const pending = pendingPulls.get(requestId)
301
+ if (pending) {
302
+ clearTimeout(pending.timer)
303
+ pendingPulls.delete(requestId)
304
+ }
305
+ throw new Error('p2p: rep_sync_req send failed')
306
+ }
307
+ return await resultPromise
308
+ }
309
+
310
+ /**
311
+ * @returns {void}
312
+ */
313
+ export function resetReputationSyncForTests() {
314
+ syncConfig = null
315
+ detachReputationSyncWire()
316
+ pendingPulls.clear()
317
+ pullTimeoutMs = 8000
318
+ }
@@ -0,0 +1,21 @@
1
+ import { getNodeTransportSettings, saveNodeTransportSettings } from './identity.mjs'
2
+
3
+ /** @typedef {'default' | 'low'} RoutingProfile */
4
+
5
+ /**
6
+ * @param {RoutingProfile} profile - `default` 或 `low`(省电)
7
+ * @returns {RoutingProfile} 写入后的当前 profile
8
+ */
9
+ export function setRoutingProfile(profile) {
10
+ if (profile !== 'default' && profile !== 'low')
11
+ throw new Error('p2p: setRoutingProfile expects default|low')
12
+ saveNodeTransportSettings({ batterySaver: profile === 'low' })
13
+ return getRoutingProfile()
14
+ }
15
+
16
+ /**
17
+ * @returns {RoutingProfile} 当前路由 profile
18
+ */
19
+ export function getRoutingProfile() {
20
+ return getNodeTransportSettings().batterySaver ? 'low' : 'default'
21
+ }
@@ -1,32 +1,51 @@
1
1
  import process from 'node:process'
2
2
 
3
- /** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
3
+ /** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
4
4
 
5
5
  /**
6
6
  * @typedef {{
7
7
  * relayOverride: string[] | null
8
- * mdnsPolicy: MdnsCandidatePolicy
8
+ * iceLocalHostnamePolicy: IceLocalHostnamePolicy
9
9
  * trickleIceOff: boolean
10
10
  * }} SignalingRuntimeConfig
11
11
  */
12
12
 
13
+ const ICE_LOCAL_HOSTNAME_POLICIES = new Set(['none', 'rewrite-loopback', 'drop'])
14
+
13
15
  /**
14
- * 生产默认:win32 丢弃 .local host candidate;其它平台不过滤。
15
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
16
+ * 生产默认:win32 丢弃 `.local` host candidate;其它平台不过滤。
17
+ * @returns {SignalingRuntimeConfig} 默认信令运行时配置
16
18
  */
17
19
  export function defaultSignalingRuntimeConfig() {
18
- const mdnsPolicy = process.platform === 'win32' ? 'drop' : 'none'
20
+ const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
19
21
  return {
20
22
  relayOverride: null,
21
- mdnsPolicy,
22
- trickleIceOff: mdnsPolicy !== 'none',
23
+ iceLocalHostnamePolicy,
24
+ trickleIceOff: iceLocalHostnamePolicy !== 'none',
23
25
  }
24
26
  }
25
27
 
26
28
  /**
27
- * 生产默认信令配置;测试通过 initNode({ signaling }) 注入。
28
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
29
+ * @param {Partial<SignalingRuntimeConfig>} [patch] 合并字段
30
+ * @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
29
31
  */
30
- export function resolveSignalingRuntimeConfig() {
31
- return defaultSignalingRuntimeConfig()
32
+ export function resolveSignalingRuntimeConfig(patch = {}) {
33
+ const base = defaultSignalingRuntimeConfig()
34
+ if (!patch || typeof patch !== 'object') return base
35
+ const policyRaw = patch.iceLocalHostnamePolicy
36
+ const iceLocalHostnamePolicy = ICE_LOCAL_HOSTNAME_POLICIES.has(/** @type {string} */ policyRaw)
37
+ ? /** @type {IceLocalHostnamePolicy} */ policyRaw
38
+ : base.iceLocalHostnamePolicy
39
+ let { relayOverride } = base
40
+ if (Object.prototype.hasOwnProperty.call(patch, 'relayOverride'))
41
+ relayOverride = patch.relayOverride == null
42
+ ? null
43
+ : [...new Set((Array.isArray(patch.relayOverride) ? patch.relayOverride : [])
44
+ .map(url => String(url || '').trim())
45
+ .filter(url => url.startsWith('wss://')))]
46
+ return {
47
+ relayOverride,
48
+ iceLocalHostnamePolicy,
49
+ trickleIceOff: patch.trickleIceOff !== undefined ? !!patch.trickleIceOff : iceLocalHostnamePolicy !== 'none',
50
+ }
32
51
  }
package/overlay/index.mjs CHANGED
@@ -1,11 +1,31 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
4
- import { randomMsgIdHex } from '../link/frame.mjs'
4
+ import { randomFrameIdHex } from '../link/frame.mjs'
5
5
  import { createLruMap } from '../utils/lru.mjs'
6
6
 
7
7
  const ROUTE_DOMAIN = 'fount-route'
8
8
 
9
+ /** @type {((senderNodeHash: string, action: string) => boolean) | null} */
10
+ let overlayRateGate = null
11
+
12
+ /**
13
+ * 安装 overlay 入站限速门(返回 false 则丢弃)。
14
+ * @param {((senderNodeHash: string, action: string) => boolean) | null} rateGate 返回 false 则丢弃
15
+ * @returns {void}
16
+ */
17
+ export function setOverlayRateGate(rateGate) {
18
+ overlayRateGate = typeof rateGate === 'function' ? rateGate : null
19
+ }
20
+
21
+ /**
22
+ * 清除 overlay 限速门。
23
+ * @returns {void}
24
+ */
25
+ export function clearOverlayRateGate() {
26
+ overlayRateGate = null
27
+ }
28
+
9
29
  /**
10
30
  * 构造 overlay 路由签名用的字节序列。
11
31
  * @param {string} reqId 路由请求 id
@@ -27,7 +47,7 @@ export function createOverlayRouter(registry, ttl = 3) {
27
47
  const selfPubKey = registry.localIdentity.nodePubKey
28
48
  const { secretKey } = registry.localIdentity
29
49
  const seenReqs = createLruMap(4096)
30
- /** @type {Map<string, { resolve: (path: string[]) => void, reject: (err: Error) => void, timer: number }>} */
50
+ /** @type {Map<string, { resolve: (path: string[]) => void, reject: (error: Error) => void, timer: number }>} */
31
51
  const pendingRoutes = new Map()
32
52
  /** @type {Set<(body: unknown, meta: { path: string[], from: string }) => void>} */
33
53
  const relayListeners = new Set()
@@ -50,12 +70,13 @@ export function createOverlayRouter(registry, ttl = 3) {
50
70
  */
51
71
  async function handleOverlay(senderNodeHash, envelope) {
52
72
  const payload = envelope?.payload
53
- const action = String(envelope?.action || '')
54
- if (!payload || typeof payload !== 'object') return
73
+ const action = envelope?.action || ''
74
+ if (!payload) return
75
+ if (overlayRateGate && !overlayRateGate(senderNodeHash, action)) return
55
76
  if (action === 'route_req') {
56
- const reqId = String(payload.reqId || '')
57
- const target = String(payload.target || '')
58
- const hops = Array.isArray(payload.path) ? payload.path.map(String) : []
77
+ const reqId = payload.reqId || ''
78
+ const target = payload.target || ''
79
+ const hops = payload.path || []
59
80
  const remainingTtl = Number(payload.ttl)
60
81
  if (!reqId || !target || !hops.length || remainingTtl <= 0) return
61
82
  if (seenReqs.has(reqId) || hops.includes(selfNodeHash) || hops.length > 6) return
@@ -86,10 +107,10 @@ export function createOverlayRouter(registry, ttl = 3) {
86
107
  return
87
108
  }
88
109
  if (action === 'route_resp') {
89
- const reqId = String(payload.reqId || '')
90
- const path = Array.isArray(payload.path) ? payload.path.map(String) : []
91
- const nodePubKey = String(payload.nodePubKey || '')
92
- const sigHex = String(payload.sig || '')
110
+ const reqId = payload.reqId || ''
111
+ const path = payload.path || []
112
+ const nodePubKey = payload.nodePubKey || ''
113
+ const sigHex = payload.sig || ''
93
114
  if (!reqId || path.length < 2 || !nodePubKey || !sigHex) return
94
115
  if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== path[path.length - 1]) return
95
116
  const ok = await verify(Buffer.from(sigHex, 'hex'), routeSignBytes(reqId, path), Buffer.from(nodePubKey, 'hex'))
@@ -108,7 +129,7 @@ export function createOverlayRouter(registry, ttl = 3) {
108
129
  return
109
130
  }
110
131
  if (action === 'relay') {
111
- const path = Array.isArray(payload.path) ? payload.path.map(String) : []
132
+ const path = payload.path || []
112
133
  const index = Number(payload.idx)
113
134
  if (!path.length || path[index] !== selfNodeHash) return
114
135
  if (index === path.length - 1) {
@@ -137,7 +158,7 @@ export function createOverlayRouter(registry, ttl = 3) {
137
158
  * @returns {Promise<string[]>} 从本节点到目标的 nodeHash 路径
138
159
  */
139
160
  async discoverRoute(targetNodeHash, options = {}) {
140
- const reqId = randomMsgIdHex()
161
+ const reqId = randomFrameIdHex()
141
162
  const maxTtl = Number(options.ttl) || ttl
142
163
  const timeoutMs = Number(options.timeoutMs) || 10_000
143
164
  const promise = new Promise((resolve, reject) => {
@@ -164,7 +185,7 @@ export function createOverlayRouter(registry, ttl = 3) {
164
185
  * @returns {Promise<void>}
165
186
  */
166
187
  async relay(path, body) {
167
- if (!Array.isArray(path) || path[0] !== selfNodeHash || path.length < 2)
188
+ if (path?.[0] !== selfNodeHash || path.length < 2)
168
189
  throw new Error('overlay: invalid relay path')
169
190
  await sendOverlay(path[1], { action: 'relay', path, idx: 1, body })
170
191
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -30,8 +30,13 @@
30
30
  "!sim/**",
31
31
  "!scripts/**"
32
32
  ],
33
+ "bin": {
34
+ "fount-p2p": "./infra/cli.mjs"
35
+ },
33
36
  "exports": {
34
37
  ".": "./index.mjs",
38
+ "./infra": "./infra/service.mjs",
39
+ "./link": "./link/providers/index.mjs",
35
40
  "./core/*": "./core/*.mjs",
36
41
  "./crypto": "./crypto/crypto.mjs",
37
42
  "./crypto/*": "./crypto/*.mjs",
@@ -42,7 +47,12 @@
42
47
  "./discovery/bt": "./discovery/bt/index.mjs",
43
48
  "./discovery/bt/*": "./discovery/bt/*.mjs",
44
49
  "./discovery/*": "./discovery/*.mjs",
45
- "./transport/*": "./transport/*.mjs",
50
+ "./transport/link_registry": "./transport/link_registry.mjs",
51
+ "./transport/user_room": "./transport/user_room.mjs",
52
+ "./transport/group_link_set": "./transport/group_link_set.mjs",
53
+ "./transport/node_scope": "./transport/node_scope.mjs",
54
+ "./transport/room_scopes": "./transport/room_scopes.mjs",
55
+ "./transport/remote_user_room": "./transport/remote_user_room.mjs",
46
56
  "./rooms/*": "./rooms/*.mjs",
47
57
  "./overlay": "./overlay/index.mjs",
48
58
  "./overlay/*": "./overlay/*.mjs",
@@ -62,6 +72,7 @@
62
72
  "dependencies": {
63
73
  "@noble/curves": "latest",
64
74
  "node-datachannel": "latest",
75
+ "on-shutdown": "latest",
65
76
  "ws": "latest"
66
77
  },
67
78
  "devDependencies": {