@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
package/wire/part_query.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
/**
|
package/discovery/mdns.mjs
DELETED
|
@@ -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
|
-
}
|