@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,279 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import dgram from 'node:dgram'
3
+
4
+ import { base64ToBytes, bytesToBase64 } from '../core/bytes_codec.mjs'
5
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
6
+ import { nodeDebug, shortHash } from '../node/log.mjs'
7
+
8
+ import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
9
+ import { ingestNetworkAdvert } from './adverts.mjs'
10
+ import { listMulticastIpv4Addresses } from './lan_interfaces.mjs'
11
+ import { getLanPeerHint } from './lan_peer_hints.mjs'
12
+
13
+ const DEFAULT_PORT = 53531
14
+ const DEFAULT_GROUP = '239.255.42.99'
15
+ const BEACON_INTERVAL_MS = 30_000
16
+
17
+ /** @type {Map<string, number>} nodeHash → lastSeenAt */
18
+ const visibleByHash = new Map()
19
+
20
+ /**
21
+ * 记入 LAN 可见池。
22
+ * @param {string} nodeHash 节点 hash
23
+ * @param {number} [now=Date.now()] 当前时间
24
+ * @returns {void}
25
+ */
26
+ export function noteLanVisibleNode(nodeHash, now = Date.now()) {
27
+ const hash = normalizeHex64(nodeHash)
28
+ if (!isHex64(hash)) return
29
+ visibleByHash.set(hash, now)
30
+ }
31
+
32
+ /**
33
+ * 列出未过期的 LAN 可见 nodeHash。
34
+ * @param {number} [now=Date.now()] 当前时间
35
+ * @param {number} [ttlMs=BEACON_INTERVAL_MS * 3] TTL
36
+ * @returns {string[]} 可见 nodeHash 列表
37
+ */
38
+ export function listLanVisibleNodeHashes(now = Date.now(), ttlMs = BEACON_INTERVAL_MS * 3) {
39
+ /** @type {string[]} */
40
+ const out = []
41
+ for (const [hash, seenAt] of visibleByHash)
42
+ if (now - seenAt <= ttlMs) out.push(hash)
43
+ else visibleByHash.delete(hash)
44
+
45
+ return out
46
+ }
47
+
48
+ /** @returns {void} 测试用 */
49
+ export function clearLanVisibleNodes() {
50
+ visibleByHash.clear()
51
+ }
52
+
53
+ /**
54
+ * Untrusted ingress:验签 network advert 后写入可见池 / peer hint。
55
+ * @param {Uint8Array} advertBytes 加密 network advert
56
+ * @param {{ address?: string, skipNodeHash?: string }} [meta] 发送方地址 / 本机 nodeHash(过滤自回环)
57
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
58
+ */
59
+ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
60
+ if (!advertBytes?.byteLength) return null
61
+ const ingested = await ingestNetworkAdvert(advertBytes, meta)
62
+ if (!ingested) return null
63
+ const skipHash = meta.skipNodeHash ? normalizeHex64(meta.skipNodeHash) : null
64
+ if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
65
+ const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
66
+ noteLanVisibleNode(ingested.verifiedNodeHash)
67
+ noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
68
+ if (firstSeen) {
69
+ const host = String(meta.address || '').trim()
70
+ nodeDebug('p2p:lan peer visible', {
71
+ peer: shortHash(ingested.verifiedNodeHash),
72
+ host: host || undefined,
73
+ tcpPort: ingested.body?.tcpPort,
74
+ })
75
+ }
76
+ return ingested
77
+ }
78
+
79
+ /**
80
+ * LAN UDP presence:段内 beacon,非 topic 订阅模型。
81
+ * @param {{ port?: number, group?: string }} [options] 配置
82
+ * @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
83
+ */
84
+ export function createLanDiscoveryProvider(options = {}) {
85
+ const port = Number(options.port) || DEFAULT_PORT
86
+ const group = String(options.group || DEFAULT_GROUP)
87
+ /** @type {import('node:dgram').Socket | null} */
88
+ let socket = null
89
+ let bound = false
90
+ let bindPromise = null
91
+ let refs = 0
92
+ /** @type {ReturnType<typeof setInterval> | null} */
93
+ let beaconTimer = null
94
+ /** @type {string | null} */
95
+ let selfNodeHash = null
96
+ /** @type {Set<string>} */
97
+ const joinedAddresses = new Set()
98
+
99
+ /**
100
+ * @param {import('node:dgram').Socket} sock UDP socket
101
+ * @returns {void}
102
+ */
103
+ function ensureMemberships(sock) {
104
+ for (const addr of listMulticastIpv4Addresses()) {
105
+ if (joinedAddresses.has(addr)) continue
106
+ try {
107
+ sock.addMembership(group, addr)
108
+ joinedAddresses.add(addr)
109
+ }
110
+ catch { /* ignore per-interface join failure */ }
111
+ }
112
+ if (!joinedAddresses.size)
113
+ try { sock.addMembership(group) } catch { /* ignore */ }
114
+ }
115
+ function getSocket() {
116
+ return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
117
+ }
118
+
119
+ /**
120
+ * @returns {Promise<void>}
121
+ */
122
+ async function ensureBound() {
123
+ if (bound) return
124
+ if (!bindPromise)
125
+ bindPromise = (async () => {
126
+ const sock = getSocket()
127
+ await new Promise((resolve, reject) => {
128
+ sock.once('error', reject)
129
+ sock.bind(port, '0.0.0.0', () => {
130
+ sock.off('error', reject)
131
+ ensureMemberships(sock)
132
+ sock.setMulticastTTL(1)
133
+ resolve()
134
+ })
135
+ })
136
+ if (refs <= 0) {
137
+ try { sock.close() } catch { /* ignore */ }
138
+ if (socket === sock) socket = null
139
+ bound = false
140
+ joinedAddresses.clear()
141
+ return
142
+ }
143
+ sock.on('message', (raw, rinfo) => {
144
+ let packet
145
+ try { packet = JSON.parse(String(raw)) } catch { return }
146
+ if (packet?.type !== 'presence') return
147
+ let advertBytes
148
+ try {
149
+ advertBytes = packet.advertBytes
150
+ ? base64ToBytes(packet.advertBytes)
151
+ : null
152
+ }
153
+ catch { return }
154
+ if (!advertBytes?.byteLength) return
155
+ void acceptLanPresenceAdvert(advertBytes, {
156
+ address: rinfo?.address,
157
+ provider: 'lan',
158
+ skipNodeHash: selfNodeHash || undefined,
159
+ }).catch(() => { })
160
+ })
161
+ bound = true
162
+ })().finally(() => {
163
+ if (!bound) bindPromise = null
164
+ })
165
+ await bindPromise
166
+ }
167
+
168
+ /**
169
+ * @param {object} beacon beacon 载荷
170
+ * @returns {Promise<void>}
171
+ */
172
+ async function multicastBeacon(beacon) {
173
+ await ensureBound()
174
+ const sock = getSocket()
175
+ ensureMemberships(sock)
176
+ const packet = Buffer.from(JSON.stringify({ type: 'presence', ...beacon }))
177
+ const addrs = listMulticastIpv4Addresses()
178
+ /**
179
+ * @param {string | undefined} ifaceAddr 组播出口地址
180
+ * @returns {Promise<void>}
181
+ */
182
+ const sendOnce = ifaceAddr => new Promise((resolve, reject) => {
183
+ if (ifaceAddr) sock.setMulticastInterface(ifaceAddr)
184
+ sock.send(packet, port, group, error => error ? reject(error) : resolve())
185
+ })
186
+ if (!addrs.length) {
187
+ await sendOnce()
188
+ return
189
+ }
190
+ let sent = 0
191
+ /** @type {unknown} */
192
+ let lastError = null
193
+ for (const addr of addrs) {
194
+ try {
195
+ await sendOnce(addr)
196
+ sent++
197
+ }
198
+ catch (error) {
199
+ lastError = error
200
+ }
201
+ }
202
+ if (!sent) throw lastError || new Error('p2p: lan multicast send failed')
203
+ }
204
+
205
+ /**
206
+ * @returns {() => void} 释放引用;引用归零时关闭 socket
207
+ */
208
+ function acquire() {
209
+ refs++
210
+ void ensureBound().catch(() => { })
211
+ return () => {
212
+ refs = Math.max(0, refs - 1)
213
+ if (refs > 0 || !socket) return
214
+ if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
215
+ selfNodeHash = null
216
+ joinedAddresses.clear()
217
+ try { socket.close() } catch { /* ignore */ }
218
+ socket = null
219
+ bound = false
220
+ bindPromise = null
221
+ }
222
+ }
223
+
224
+ return {
225
+ id: 'lan',
226
+ priority: 10,
227
+ caps: { canDiscover: true, canSignal: false, canRelay: false },
228
+ /**
229
+ * @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
230
+ * @returns {Promise<string[]>} 可见 nodeHash;群扫描时 LAN 无群语义,返回空
231
+ */
232
+ async listVisibleNodeHashes(options = {}) {
233
+ if (options.roomSecret) return []
234
+ const limit = Math.max(1, Number(options.limit) || 64)
235
+ await ensureBound().catch(() => { })
236
+ return listLanVisibleNodeHashes().slice(0, limit)
237
+ },
238
+ /**
239
+ * 有 LAN peer hint 则可经 lan_tcp 拨号(门面 dialer 完成建链)。
240
+ * @param {string} nodeHash 目标
241
+ * @returns {Promise<boolean>} 存在 peer hint 时为 true
242
+ */
243
+ async connectToNode(nodeHash) {
244
+ const hash = normalizeHex64(nodeHash)
245
+ return isHex64(hash) && !!getLanPeerHint(hash)
246
+ },
247
+ /**
248
+ * @param {() => Promise<{ nodeHash?: string, tcpPort?: number, advertBytes?: Uint8Array, advertBody?: object } | null>} getBeacon 本机 beacon
249
+ * @returns {Promise<() => void>} 停止函数
250
+ */
251
+ async startPresence(getBeacon) {
252
+ const release = acquire()
253
+ /**
254
+ * @returns {Promise<void>}
255
+ */
256
+ const send = async () => {
257
+ const body = await getBeacon?.()
258
+ if (!body) return
259
+ if (body.nodeHash) selfNodeHash = normalizeHex64(body.nodeHash)
260
+ const advertBytes = body.advertBytes?.byteLength
261
+ ? body.advertBytes
262
+ : null
263
+ if (!advertBytes?.byteLength) return
264
+ await multicastBeacon({
265
+ nodeHash: body.nodeHash,
266
+ tcpPort: body.tcpPort,
267
+ advertBytes: bytesToBase64(advertBytes),
268
+ })
269
+ }
270
+ void send().catch(() => { })
271
+ beaconTimer = setInterval(() => { void send().catch(() => { }) }, BEACON_INTERVAL_MS)
272
+ beaconTimer.unref?.()
273
+ return () => {
274
+ if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
275
+ release()
276
+ }
277
+ },
278
+ }
279
+ }
@@ -0,0 +1,78 @@
1
+ import os from 'node:os'
2
+
3
+ /** advert / 组播 beacon 携带的 LAN IPv4 上限 */
4
+ export const MAX_LAN_HOSTS = 4
5
+
6
+ const IPV4_RE = /^(?:\d{1,3}\.){3}\d{1,3}$/
7
+
8
+ /**
9
+ * untrusted ingress:清洗 advert body 中的 LAN IPv4 列表。
10
+ * @param {unknown} input 原始 lanHosts
11
+ * @returns {string[]} 去重后的 IPv4 列表
12
+ */
13
+ export function normalizeLanHosts(input) {
14
+ if (!input) return []
15
+ const arr = Array.isArray(input) ? input : [input]
16
+ const seen = new Set()
17
+ /** @type {string[]} */
18
+ const out = []
19
+ for (const item of arr) {
20
+ const host = String(item || '').trim()
21
+ if (!host || !IPV4_RE.test(host) || seen.has(host)) continue
22
+ seen.add(host)
23
+ out.push(host)
24
+ if (out.length >= MAX_LAN_HOSTS) break
25
+ }
26
+ return out
27
+ }
28
+
29
+ /**
30
+ * @param {string} addr IPv4
31
+ * @returns {number} 排序权重(越小越优先)
32
+ */
33
+ function lanAddressRank(addr) {
34
+ if (addr.startsWith('169.254.')) return 100
35
+ if (addr.startsWith('192.168.56.')) return 90
36
+ if (addr.startsWith('192.168.')) return 10
37
+ if (addr.startsWith('10.')) return 20
38
+ const match = /^172\.(\d+)\./u.exec(addr)
39
+ if (match && Number(match[1]) >= 16 && Number(match[1]) <= 31) return 30
40
+ return 50
41
+ }
42
+
43
+ /**
44
+ * @param {string[]} addrs IPv4 列表
45
+ * @returns {string[]} 按 LAN 可达性优先排序
46
+ */
47
+ function prioritizeLanAddresses(addrs) {
48
+ return [...addrs].sort((left, right) => lanAddressRank(left) - lanAddressRank(right))
49
+ }
50
+
51
+ /**
52
+ * @param {string | number} family os.networkInterfaces family
53
+ * @returns {boolean} 是否 IPv4
54
+ */
55
+ function isIpv4Family(family) {
56
+ return family === 'IPv4' || family === 4
57
+ }
58
+
59
+ /**
60
+ * 本机可用于 LAN 组播 / advert 的非 internal IPv4 地址。
61
+ * @returns {string[]} 去重、排序后的地址列表
62
+ */
63
+ export function listMulticastIpv4Addresses() {
64
+ const seen = new Set()
65
+ /** @type {string[]} */
66
+ const addrs = []
67
+ for (const ifaces of Object.values(os.networkInterfaces())) {
68
+ if (!ifaces) continue
69
+ for (const iface of ifaces) {
70
+ if (iface.internal || !isIpv4Family(iface.family)) continue
71
+ const addr = String(iface.address || '').trim()
72
+ if (!addr || seen.has(addr)) continue
73
+ seen.add(addr)
74
+ addrs.push(addr)
75
+ }
76
+ }
77
+ return prioritizeLanAddresses(addrs).slice(0, MAX_LAN_HOSTS)
78
+ }
@@ -5,11 +5,15 @@ import { createTtlMap } from '../utils/ttl_map.mjs'
5
5
  /** LAN peer hint 存活时间。 */
6
6
  export const LAN_PEER_HINT_TTL_MS = 5 * 60_000
7
7
 
8
- /** @type {ReturnType<typeof createTtlMap<{ host: string, port: number }>>} */
8
+ /** peer 保留的 endpoint 上限。 */
9
+ const MAX_ENDPOINTS = 8
10
+
11
+ /** @type {ReturnType<typeof createTtlMap<{ endpoints: { host: string, port: number }[] }>>} */
9
12
  const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
10
13
 
11
14
  /**
12
15
  * 记录 LAN 上观察到的 nodeHash → host:port。
16
+ * 最新写入排在最前(dial / getLanPeerHint 优先用新观测)。
13
17
  * @param {string} nodeHash 节点 64 hex
14
18
  * @param {{ host: string, port: number }} endpoint 端点
15
19
  * @returns {void}
@@ -19,19 +23,32 @@ export function noteLanPeerHint(nodeHash, endpoint) {
19
23
  const host = String(endpoint?.host || '').trim()
20
24
  const port = normalizeTcpPort(endpoint?.port)
21
25
  if (!hash || !host || !port) return
22
- hints.set(hash, { host, port })
26
+ const existing = hints.get(hash)?.endpoints ?? []
27
+ const next = existing.filter(item => !(item.host === host && item.port === port))
28
+ next.unshift({ host, port })
29
+ hints.set(hash, { endpoints: next.slice(0, MAX_ENDPOINTS) })
23
30
  }
24
31
 
25
32
  /**
26
- * 查询未过期的 LAN peer hint
33
+ * 查询未过期的首个 LAN peer hint(最新观测)。
27
34
  * @param {string} nodeHash 节点 64 hex
28
35
  * @param {number} [now=Date.now()] 当前时间(测试可注入)
29
36
  * @returns {{ host: string, port: number } | null} hint 或 null
30
37
  */
31
38
  export function getLanPeerHint(nodeHash, now = Date.now()) {
39
+ return listLanPeerHints(nodeHash, now)[0] ?? null
40
+ }
41
+
42
+ /**
43
+ * 查询未过期的全部 LAN peer hint(最新在前)。
44
+ * @param {string} nodeHash 节点 64 hex
45
+ * @param {number} [now=Date.now()] 当前时间(测试可注入)
46
+ * @returns {{ host: string, port: number }[]} hint 列表
47
+ */
48
+ export function listLanPeerHints(nodeHash, now = Date.now()) {
32
49
  const hash = normalizeHex64(nodeHash)
33
- if (!hash) return null
34
- return hints.get(hash, now)
50
+ if (!hash) return []
51
+ return hints.get(hash, now)?.endpoints ?? []
35
52
  }
36
53
 
37
54
  /**