@steve02081504/fount-p2p 0.0.12 → 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 (75) 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 +1 -1
  7. package/discovery/adverts.mjs +109 -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 +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
@@ -9,9 +9,9 @@ import {
9
9
  parsePartQueryReq,
10
10
  parsePartQueryRes,
11
11
  } from '../schemas/part_query.mjs'
12
+ import { sendToNodeLink } from '../transport/link_registry.mjs'
12
13
  import { buildMergedGraph } from '../trust_graph/build.mjs'
13
14
  import { pickTopFromGraph } from '../trust_graph/engine.mjs'
14
- import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
15
15
  import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
16
16
  import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
17
17
 
@@ -210,8 +210,9 @@ async function selectQueryNeighbors(username, exclude, dependencies) {
210
210
  * @returns {Promise<boolean>} 是否发出
211
211
  */
212
212
  async function deliverQuery(username, nodeHash, action, payload, dependencies) {
213
+ void username
213
214
  if (dependencies.deliver) return Boolean(await dependencies.deliver(nodeHash, action, payload))
214
- return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, nodeHash, action, payload)
215
+ return sendToNodeLink(nodeHash, { scope: 'node', action, payload })
215
216
  }
216
217
 
217
218
  /**
@@ -326,27 +327,32 @@ async function processIncomingRequest(wireContext, wire, request, peerId, depend
326
327
  * @param {{ replicaUsername?: string }} wireContext 入站上下文
327
328
  * @param {PartWireAdapter} wire wire
328
329
  * @param {PartQueryDependencies} [dependencies] 可注入依赖(含 per-node state)
329
- * @returns {void}
330
+ * @returns {() => void} 取消挂载的 dispose
330
331
  */
331
332
  export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
332
333
  const state = resolveState(dependencies)
333
- wire.on('part_query_req', (data, peerId) => {
334
- if (!isPlainObject(data)) return
335
- const request = parsePartQueryReq(data)
336
- if (!request) return
337
- if (!state.takeDedupe(request.requestId)) return
338
- const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
339
- if (source && !consumeWireRateBucket(`part_query:${source}`, {
340
- maxCount: partQueryTunables.ratePerSourcePerMin,
341
- })) return
342
- void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
343
- })
344
-
345
- wire.on('part_query_res', (data, peerId) => {
346
- const response = parsePartQueryRes(data)
347
- if (!response) return
348
- handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
349
- })
334
+ const offs = [
335
+ wire.on('part_query_req', (data, peerId) => {
336
+ if (!isPlainObject(data)) return
337
+ const request = parsePartQueryReq(data)
338
+ if (!request) return
339
+ if (!state.takeDedupe(request.requestId)) return
340
+ const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
341
+ if (source && !consumeWireRateBucket(`part_query:${source}`, {
342
+ maxCount: partQueryTunables.ratePerSourcePerMin,
343
+ })) return
344
+ void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
345
+ }),
346
+ wire.on('part_query_res', (data, peerId) => {
347
+ const response = parsePartQueryRes(data)
348
+ if (!response) return
349
+ handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
350
+ }),
351
+ ]
352
+ return () => {
353
+ for (const off of offs)
354
+ try { off?.() } catch { /* ignore */ }
355
+ }
350
356
  }
351
357
 
352
358
  /**
@@ -1,197 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
- import dgram from 'node:dgram'
3
-
4
- const DEFAULT_PORT = 53531
5
- const DEFAULT_GROUP = '239.255.42.99'
6
-
7
- /**
8
- * 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
9
- * 引用计数为 0 时关闭 UDP socket,避免 shutdown 后句柄拖住进程。
10
- *
11
- * @param {{ port?: number, group?: string }} [options] 组播端口与组地址
12
- * @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
13
- */
14
- export function createMdnsDiscoveryProvider(options = {}) {
15
- const port = Number(options.port) || DEFAULT_PORT
16
- const group = String(options.group || DEFAULT_GROUP)
17
- /** @type {import('node:dgram').Socket | null} */
18
- let socket = null
19
- let bound = false
20
- let bindPromise = null
21
- let refs = 0
22
- /** @type {Map<string, Set<Function>>} */
23
- const advertListeners = new Map()
24
- /** @type {Map<string, Set<Function>>} */
25
- const signalListeners = new Map()
26
-
27
- /**
28
- * 懒创建或返回当前 UDP socket。
29
- * @returns {import('node:dgram').Socket} 用于组播 bind/send 的 socket
30
- */
31
- function getSocket() {
32
- return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
33
- }
34
-
35
- /**
36
- * 绑定 UDP socket 并注册组播消息处理器。
37
- * @returns {Promise<void>}
38
- */
39
- async function ensureBound() {
40
- if (bound) return
41
- if (!bindPromise)
42
- bindPromise = (async () => {
43
- const sock = getSocket()
44
- await new Promise((resolve, reject) => {
45
- sock.once('error', reject)
46
- sock.bind(port, '0.0.0.0', () => {
47
- sock.off('error', reject)
48
- sock.addMembership(group)
49
- sock.setMulticastTTL(1)
50
- resolve()
51
- })
52
- })
53
- // acquire 已全部释放:关掉刚 bind 的 socket,避免泄漏句柄。
54
- if (refs <= 0) {
55
- try { sock.close() } catch { /* ignore */ }
56
- if (socket === sock) socket = null
57
- bound = false
58
- return
59
- }
60
- sock.on('message', (raw, rinfo) => {
61
- let packet
62
- try { packet = JSON.parse(String(raw)) } catch { return }
63
- const listeners = packet.type === 'advert'
64
- ? advertListeners.get(String(packet.topic || ''))
65
- : signalListeners.get(String(packet.topic || ''))
66
- if (!listeners?.size) return
67
- const bytes = Uint8Array.from(Buffer.from(String(packet.data || ''), 'base64'))
68
- const meta = {
69
- provider: 'mdns',
70
- address: String(rinfo?.address || ''),
71
- }
72
- for (const listener of listeners)
73
- listener(bytes, meta)
74
- })
75
- bound = true
76
- })()
77
- .finally(() => {
78
- if (!bound) bindPromise = null
79
- })
80
- await bindPromise
81
- }
82
-
83
- /**
84
- * 向组播组发送 advert 或 signal 包。
85
- * @param {'advert' | 'signal'} type 包类型
86
- * @param {string} topic topic 名称
87
- * @param {Uint8Array} bytes 载荷字节
88
- * @returns {Promise<void>}
89
- */
90
- async function multicast(type, topic, bytes) {
91
- await ensureBound()
92
- const sock = getSocket()
93
- const packet = Buffer.from(JSON.stringify({
94
- type,
95
- topic,
96
- data: Buffer.from(bytes).toString('base64'),
97
- }))
98
- await new Promise((resolve, reject) => {
99
- sock.send(packet, port, group, error => error ? reject(error) : resolve())
100
- })
101
- }
102
-
103
- /**
104
- * 占用 socket;归还后若无引用则关闭。
105
- * @returns {() => void} 释放函数
106
- */
107
- function acquire() {
108
- refs++
109
- void ensureBound().catch(() => { })
110
- return () => {
111
- refs = Math.max(0, refs - 1)
112
- if (refs > 0 || !socket) return
113
- try { socket.close() } catch { /* ignore */ }
114
- socket = null
115
- bound = false
116
- bindPromise = null
117
- }
118
- }
119
-
120
- /**
121
- * 向 topic bucket 注册监听器。
122
- * @param {Map<string, Set<Function>>} bucket topic → 监听器集合
123
- * @param {string} topic 订阅 topic
124
- * @param {Function} listener 回调函数
125
- * @returns {() => void} 取消订阅函数
126
- */
127
- function addListener(bucket, topic, listener) {
128
- if (!bucket.has(topic)) bucket.set(topic, new Set())
129
- bucket.get(topic).add(listener)
130
- const release = acquire()
131
- return () => {
132
- const set = bucket.get(topic)
133
- if (!set) {
134
- release()
135
- return
136
- }
137
- set.delete(listener)
138
- if (!set.size) bucket.delete(topic)
139
- release()
140
- }
141
- }
142
-
143
- return {
144
- id: 'mdns',
145
- priority: 10,
146
- caps: { canDiscover: true, canSignal: true, canRelay: false },
147
- /**
148
- * 周期性组播广播 advert(首发后台,不阻塞调用方)。
149
- * @param {string} topic advert 主题
150
- * @param {Uint8Array} bytes advert 载荷
151
- * @returns {Promise<() => void>} 取消广播函数
152
- */
153
- async advertise(topic, bytes) {
154
- const release = acquire()
155
- void multicast('advert', topic, bytes).catch(() => { })
156
- const timer = setInterval(() => { void multicast('advert', topic, bytes).catch(() => { }) }, 30_000)
157
- return () => {
158
- clearInterval(timer)
159
- release()
160
- }
161
- },
162
- /**
163
- * 订阅组播 advert(立即返回;UDP bind 后台完成)。
164
- * @param {string} topic advert 主题
165
- * @param {Function} onAdvert advert 回调
166
- * @returns {Promise<() => void>} 取消订阅函数
167
- */
168
- async subscribe(topic, onAdvert) {
169
- return addListener(advertListeners, topic, onAdvert)
170
- },
171
- /**
172
- * 组播发送信令(忽略单播目标)。
173
- * @param {string} topic 信令 topic
174
- * @param {string} _to 目标标识(未使用)
175
- * @param {Uint8Array} bytes 信令载荷
176
- * @returns {Promise<void>}
177
- */
178
- async sendSignal(topic, _to, bytes) {
179
- const release = acquire()
180
- try {
181
- await multicast('signal', topic, bytes)
182
- }
183
- finally {
184
- release()
185
- }
186
- },
187
- /**
188
- * 订阅组播信令(立即返回;UDP bind 后台完成)。
189
- * @param {string} topic 信令 topic
190
- * @param {Function} onSignal 信令回调
191
- * @returns {Promise<() => void>} 取消订阅函数
192
- */
193
- async onSignal(topic, onSignal) {
194
- return addListener(signalListeners, topic, onSignal)
195
- },
196
- }
197
- }
@@ -1,104 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
3
-
4
- import { normalizeHex64 } from '../core/hexIds.mjs'
5
- import { sha256Hex } from '../crypto/crypto.mjs'
6
- import { createLruMap } from '../utils/lru.mjs'
7
-
8
- const SIGNAL_DOMAIN = 'fount-signal'
9
- const NODE_TOPIC_DOMAIN = 'fount-rdv-node:'
10
- const GROUP_TOPIC_DOMAIN = 'fount-rdv-group:'
11
-
12
- /** topic → AES-256 密钥缓存上限(群/节点 topic 会随时间增长,须有界) */
13
- export const SIGNAL_KEY_CACHE_MAX = 512
14
-
15
- /**
16
- * 由 nodeHash 派生节点 rendezvous topic。
17
- * @param {string} nodeHash 节点 64 hex
18
- * @returns {string} rendezvous topic 哈希
19
- */
20
- export function nodeRendezvousTopic(nodeHash) {
21
- return sha256Hex(`${NODE_TOPIC_DOMAIN}${normalizeHex64(nodeHash)}`)
22
- }
23
-
24
- /**
25
- * 由房间密钥派生群组 rendezvous topic。
26
- * @param {string} roomSecret 房间密钥
27
- * @returns {string} rendezvous topic 哈希
28
- */
29
- export function groupRendezvousTopic(roomSecret) {
30
- return sha256Hex(`${GROUP_TOPIC_DOMAIN}${roomSecret}`)
31
- }
32
-
33
- /** topic → AES-256 密钥 LRU */
34
- const signalKeysByTopic = createLruMap(SIGNAL_KEY_CACHE_MAX)
35
-
36
- /**
37
- * 由 topic 派生信令 AES 密钥(同 topic 复用,避免每包 SHA-256)。
38
- * @param {string} topic rendezvous 主题
39
- * @returns {Buffer} AES-256 密钥
40
- */
41
- function signalKeyForTopic(topic) {
42
- let cached = signalKeysByTopic.get(topic)
43
- if (cached) {
44
- signalKeysByTopic.touch(topic, cached)
45
- return cached
46
- }
47
- cached = createHash('sha256').update(`${SIGNAL_DOMAIN}:${topic}`).digest()
48
- signalKeysByTopic.touch(topic, cached)
49
- return cached
50
- }
51
-
52
- /**
53
- * 当前密钥缓存条目数(测试用)。
54
- * @returns {number} 缓存大小
55
- */
56
- export function signalKeyCacheSize() {
57
- return signalKeysByTopic.size
58
- }
59
-
60
- /**
61
- * 加密信令包为 AES-GCM 字节序列。
62
- * @param {string} topic rendezvous 主题
63
- * @param {unknown} packet 待加密 JSON 对象
64
- * @returns {Uint8Array} 加密后的字节
65
- */
66
- export function encryptSignalPacket(topic, packet) {
67
- const iv = randomBytes(12)
68
- const cipher = createCipheriv('aes-256-gcm', signalKeyForTopic(topic), iv)
69
- const ciphertext = Buffer.concat([
70
- cipher.update(Buffer.from(JSON.stringify(packet), 'utf8')),
71
- cipher.final(),
72
- ])
73
- return Buffer.from(JSON.stringify({
74
- iv: iv.toString('base64'),
75
- authTag: cipher.getAuthTag().toString('base64'),
76
- ciphertext: ciphertext.toString('base64'),
77
- }))
78
- }
79
-
80
- /**
81
- * 解密信令包;失败时返回 null。
82
- * @param {string} topic rendezvous 主题
83
- * @param {Uint8Array} bytes 加密字节
84
- * @returns {object | null} 解密后的 JSON 对象
85
- */
86
- export function decryptSignalPacket(topic, bytes) {
87
- try {
88
- const payload = JSON.parse(Buffer.from(bytes).toString('utf8'))
89
- const decipher = createDecipheriv(
90
- 'aes-256-gcm',
91
- signalKeyForTopic(topic),
92
- Buffer.from(payload.iv, 'base64'),
93
- )
94
- decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'))
95
- const plain = Buffer.concat([
96
- decipher.update(Buffer.from(payload.ciphertext, 'base64')),
97
- decipher.final(),
98
- ])
99
- return JSON.parse(plain.toString('utf8'))
100
- }
101
- catch {
102
- return null
103
- }
104
- }