@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,228 @@
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 { ingestNetworkAdvert } from './adverts.mjs'
9
+ import { getLanPeerHint, noteLanPeerHint } from './lan_peer_hints.mjs'
10
+
11
+ const DEFAULT_PORT = 53531
12
+ const DEFAULT_GROUP = '239.255.42.99'
13
+ const BEACON_INTERVAL_MS = 30_000
14
+
15
+ /** @type {Map<string, number>} nodeHash → lastSeenAt */
16
+ const visibleByHash = new Map()
17
+
18
+ /**
19
+ * 记入 LAN 可见池。
20
+ * @param {string} nodeHash 节点 hash
21
+ * @param {number} [now=Date.now()] 当前时间
22
+ * @returns {void}
23
+ */
24
+ export function noteLanVisibleNode(nodeHash, now = Date.now()) {
25
+ const hash = normalizeHex64(nodeHash)
26
+ if (!isHex64(hash)) return
27
+ visibleByHash.set(hash, now)
28
+ }
29
+
30
+ /**
31
+ * 列出未过期的 LAN 可见 nodeHash。
32
+ * @param {number} [now=Date.now()] 当前时间
33
+ * @param {number} [ttlMs=BEACON_INTERVAL_MS * 3] TTL
34
+ * @returns {string[]} 可见 nodeHash 列表
35
+ */
36
+ export function listLanVisibleNodeHashes(now = Date.now(), ttlMs = BEACON_INTERVAL_MS * 3) {
37
+ /** @type {string[]} */
38
+ const out = []
39
+ for (const [hash, seenAt] of visibleByHash)
40
+ if (now - seenAt <= ttlMs) out.push(hash)
41
+ else visibleByHash.delete(hash)
42
+
43
+ return out
44
+ }
45
+
46
+ /** @returns {void} 测试用 */
47
+ export function clearLanVisibleNodes() {
48
+ visibleByHash.clear()
49
+ }
50
+
51
+ /**
52
+ * Untrusted ingress:验签 network advert 后写入可见池 / peer hint。
53
+ * @param {Uint8Array} advertBytes 加密 network advert
54
+ * @param {{ address?: string }} [meta] 发送方地址
55
+ * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
56
+ */
57
+ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
58
+ if (!advertBytes?.byteLength) return null
59
+ const ingested = await ingestNetworkAdvert(advertBytes, meta)
60
+ if (!ingested) return null
61
+ const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
62
+ noteLanVisibleNode(ingested.verifiedNodeHash)
63
+ const host = String(meta.address || '').trim()
64
+ const tcpPort = Number(ingested.body?.tcpPort)
65
+ if (host && Number.isFinite(tcpPort) && tcpPort > 0)
66
+ noteLanPeerHint(ingested.verifiedNodeHash, { host, port: tcpPort })
67
+ if (firstSeen)
68
+ nodeDebug('p2p:lan peer visible', {
69
+ peer: shortHash(ingested.verifiedNodeHash),
70
+ host: host || undefined,
71
+ tcpPort: tcpPort > 0 ? tcpPort : undefined,
72
+ })
73
+ return ingested
74
+ }
75
+
76
+ /**
77
+ * LAN UDP presence:段内 beacon,非 topic 订阅模型。
78
+ * @param {{ port?: number, group?: string }} [options] 配置
79
+ * @returns {import('./index.mjs').DiscoveryProvider} LAN 发现提供者
80
+ */
81
+ export function createLanDiscoveryProvider(options = {}) {
82
+ const port = Number(options.port) || DEFAULT_PORT
83
+ const group = String(options.group || DEFAULT_GROUP)
84
+ /** @type {import('node:dgram').Socket | null} */
85
+ let socket = null
86
+ let bound = false
87
+ let bindPromise = null
88
+ let refs = 0
89
+ /** @type {ReturnType<typeof setInterval> | null} */
90
+ let beaconTimer = null
91
+
92
+ /**
93
+ * @returns {import('node:dgram').Socket} 复用的组播 UDP socket
94
+ */
95
+ function getSocket() {
96
+ return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
97
+ }
98
+
99
+ /**
100
+ * @returns {Promise<void>}
101
+ */
102
+ async function ensureBound() {
103
+ if (bound) return
104
+ if (!bindPromise)
105
+ bindPromise = (async () => {
106
+ const sock = getSocket()
107
+ await new Promise((resolve, reject) => {
108
+ sock.once('error', reject)
109
+ sock.bind(port, '0.0.0.0', () => {
110
+ sock.off('error', reject)
111
+ sock.addMembership(group)
112
+ sock.setMulticastTTL(1)
113
+ resolve()
114
+ })
115
+ })
116
+ if (refs <= 0) {
117
+ try { sock.close() } catch { /* ignore */ }
118
+ if (socket === sock) socket = null
119
+ bound = false
120
+ return
121
+ }
122
+ sock.on('message', (raw, rinfo) => {
123
+ let packet
124
+ try { packet = JSON.parse(String(raw)) } catch { return }
125
+ if (packet?.type !== 'presence') return
126
+ let advertBytes
127
+ try {
128
+ advertBytes = packet.advertBytes
129
+ ? base64ToBytes(packet.advertBytes)
130
+ : null
131
+ }
132
+ catch { return }
133
+ if (!advertBytes?.byteLength) return
134
+ void acceptLanPresenceAdvert(advertBytes, { address: rinfo?.address, provider: 'lan' })
135
+ .catch(() => { })
136
+ })
137
+ bound = true
138
+ })().finally(() => {
139
+ if (!bound) bindPromise = null
140
+ })
141
+ await bindPromise
142
+ }
143
+
144
+ /**
145
+ * @param {object} beacon beacon 载荷
146
+ * @returns {Promise<void>}
147
+ */
148
+ async function multicastBeacon(beacon) {
149
+ await ensureBound()
150
+ const sock = getSocket()
151
+ const packet = Buffer.from(JSON.stringify({ type: 'presence', ...beacon }))
152
+ await new Promise((resolve, reject) => {
153
+ sock.send(packet, port, group, error => error ? reject(error) : resolve())
154
+ })
155
+ }
156
+
157
+ /**
158
+ * @returns {() => void} 释放引用;引用归零时关闭 socket
159
+ */
160
+ function acquire() {
161
+ refs++
162
+ void ensureBound().catch(() => { })
163
+ return () => {
164
+ refs = Math.max(0, refs - 1)
165
+ if (refs > 0 || !socket) return
166
+ if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
167
+ try { socket.close() } catch { /* ignore */ }
168
+ socket = null
169
+ bound = false
170
+ bindPromise = null
171
+ }
172
+ }
173
+
174
+ return {
175
+ id: 'lan',
176
+ priority: 10,
177
+ caps: { canDiscover: true, canSignal: false, canRelay: false },
178
+ /**
179
+ * @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
180
+ * @returns {Promise<string[]>} 可见 nodeHash;群扫描时 LAN 无群语义,返回空
181
+ */
182
+ async listVisibleNodeHashes(options = {}) {
183
+ if (options.roomSecret) return []
184
+ const limit = Math.max(1, Number(options.limit) || 64)
185
+ await ensureBound().catch(() => { })
186
+ return listLanVisibleNodeHashes().slice(0, limit)
187
+ },
188
+ /**
189
+ * 有 LAN peer hint 则可经 lan_tcp 拨号(门面 dialer 完成建链)。
190
+ * @param {string} nodeHash 目标
191
+ * @returns {Promise<boolean>} 存在 peer hint 时为 true
192
+ */
193
+ async connectToNode(nodeHash) {
194
+ const hash = normalizeHex64(nodeHash)
195
+ return isHex64(hash) && !!getLanPeerHint(hash)
196
+ },
197
+ /**
198
+ * @param {() => Promise<{ nodeHash?: string, tcpPort?: number, advertBytes?: Uint8Array, advertBody?: object } | null>} getBeacon 本机 beacon
199
+ * @returns {Promise<() => void>} 停止函数
200
+ */
201
+ async startPresence(getBeacon) {
202
+ const release = acquire()
203
+ /**
204
+ * @returns {Promise<void>}
205
+ */
206
+ const send = async () => {
207
+ const body = await getBeacon?.()
208
+ if (!body) return
209
+ const advertBytes = body.advertBytes?.byteLength
210
+ ? body.advertBytes
211
+ : null
212
+ if (!advertBytes?.byteLength) return
213
+ await multicastBeacon({
214
+ nodeHash: body.nodeHash,
215
+ tcpPort: body.tcpPort,
216
+ advertBytes: bytesToBase64(advertBytes),
217
+ })
218
+ }
219
+ void send().catch(() => { })
220
+ beaconTimer = setInterval(() => { void send().catch(() => { }) }, BEACON_INTERVAL_MS)
221
+ beaconTimer.unref?.()
222
+ return () => {
223
+ if (beaconTimer) { clearInterval(beaconTimer); beaconTimer = null }
224
+ release()
225
+ }
226
+ },
227
+ }
228
+ }