@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.
- package/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -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
|
+
}
|