@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.
- 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 +9 -3
- package/discovery/adverts.mjs +115 -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 +279 -0
- package/discovery/lan_interfaces.mjs +78 -0
- package/discovery/lan_peer_hints.mjs +22 -5
- package/discovery/nostr.mjs +474 -65
- 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 +25 -18
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/providers/lan_tcp.mjs +62 -33
- package/link/providers/webrtc.mjs +3 -1
- 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,318 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
|
+
import { sendToNodeLink } from '../transport/link_registry.mjs'
|
|
5
|
+
import { attachNodeScopeFeature, ensureNodeScope, getNodeScopeWire } from '../transport/node_scope.mjs'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
loadReputation,
|
|
9
|
+
mutateReputation,
|
|
10
|
+
} from './reputation_store.mjs'
|
|
11
|
+
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
12
|
+
|
|
13
|
+
const SYNC_DATA_NAME = 'reputation_sync'
|
|
14
|
+
const MAX_LOCKED_SCORE = 1
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* trustSyncDonors: string[]
|
|
19
|
+
* reputationExportAllowlist: string[]
|
|
20
|
+
* lockedMaxNodeHashes: string[]
|
|
21
|
+
* lockedMaxPrevByNodeHash: Record<string, number>
|
|
22
|
+
* }} ReputationSyncConfig
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** @type {ReputationSyncConfig | null} */
|
|
26
|
+
let syncConfig = null
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, { resolve: (v: object) => void, reject: (e: Error) => void, timer: ReturnType<typeof setTimeout>, donor: string }>} */
|
|
29
|
+
const pendingPulls = new Map()
|
|
30
|
+
|
|
31
|
+
/** @type {Set<() => void>} */
|
|
32
|
+
const syncWireDisposers = new Set()
|
|
33
|
+
|
|
34
|
+
let pullTimeoutMs = 8000
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {number} ms 超时毫秒;测试用
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
export function setReputationPullTimeoutMsForTests(ms) {
|
|
41
|
+
pullTimeoutMs = Math.max(1, Number(ms) || 8000)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {ReputationSyncConfig} 内存中的 sync 配置(首次从盘加载)
|
|
46
|
+
*/
|
|
47
|
+
function loadSyncConfig() {
|
|
48
|
+
if (syncConfig) return syncConfig
|
|
49
|
+
const raw = readNodeJsonSync(SYNC_DATA_NAME) || {}
|
|
50
|
+
/** @type {Record<string, number>} */
|
|
51
|
+
const lockedMaxPrevByNodeHash = {}
|
|
52
|
+
const prevRaw = raw.lockedMaxPrevByNodeHash && typeof raw.lockedMaxPrevByNodeHash === 'object'
|
|
53
|
+
? raw.lockedMaxPrevByNodeHash
|
|
54
|
+
: {}
|
|
55
|
+
for (const [nodeHash, score] of Object.entries(prevRaw)) {
|
|
56
|
+
const id = normalizeHex64(nodeHash)
|
|
57
|
+
const n = Number(score)
|
|
58
|
+
if (id && isHex64(id) && Number.isFinite(n)) lockedMaxPrevByNodeHash[id] = n
|
|
59
|
+
}
|
|
60
|
+
syncConfig = {
|
|
61
|
+
trustSyncDonors: normalizeHashList(raw.trustSyncDonors),
|
|
62
|
+
reputationExportAllowlist: normalizeHashList(raw.reputationExportAllowlist),
|
|
63
|
+
lockedMaxNodeHashes: normalizeHashList(raw.lockedMaxNodeHashes),
|
|
64
|
+
lockedMaxPrevByNodeHash,
|
|
65
|
+
}
|
|
66
|
+
return syncConfig
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {unknown} list - 原始 hash 列表
|
|
71
|
+
* @returns {string[]} 规范化去重后的 64-hex 列表
|
|
72
|
+
*/
|
|
73
|
+
function normalizeHashList(list) {
|
|
74
|
+
return [...new Set((Array.isArray(list) ? list : [])
|
|
75
|
+
.map(id => normalizeHex64(id))
|
|
76
|
+
.filter(id => isHex64(id)))]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
function persistSyncConfig() {
|
|
83
|
+
writeNodeJsonSync(SYNC_DATA_NAME, loadSyncConfig())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @returns {object} 当前 reputation 表(byNodeHash)
|
|
88
|
+
*/
|
|
89
|
+
export function getReputationTable() {
|
|
90
|
+
return loadReputation()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {object} table - 含 byNodeHash 的信誉表或裸 byNodeHash 对象
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
97
|
+
export async function setReputationTable(table) {
|
|
98
|
+
const incoming = table?.byNodeHash && typeof table.byNodeHash === 'object' ? table.byNodeHash : table
|
|
99
|
+
if (!incoming || typeof incoming !== 'object') throw new Error('p2p: setReputationTable requires byNodeHash object')
|
|
100
|
+
await mutateReputation(data => {
|
|
101
|
+
data.byNodeHash = data.byNodeHash || {}
|
|
102
|
+
for (const [nodeHash, row] of Object.entries(incoming)) {
|
|
103
|
+
const id = normalizeHex64(nodeHash)
|
|
104
|
+
if (!id || !isHex64(id)) continue
|
|
105
|
+
const score = Number(row?.score ?? row)
|
|
106
|
+
if (!Number.isFinite(score)) continue
|
|
107
|
+
data.byNodeHash[id] = { ...data.byNodeHash[id] || {}, score }
|
|
108
|
+
}
|
|
109
|
+
applyLocksToReputation(data)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} data - reputation 存储对象
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
function applyLocksToReputation(data) {
|
|
118
|
+
for (const nodeHash of loadSyncConfig().lockedMaxNodeHashes)
|
|
119
|
+
if (!data.byNodeHash[nodeHash]) data.byNodeHash[nodeHash] = { score: MAX_LOCKED_SCORE }
|
|
120
|
+
else data.byNodeHash[nodeHash].score = MAX_LOCKED_SCORE
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 将节点分数钳到上限;首次 lock 时记下原分,unlock 时还原。
|
|
125
|
+
* @param {string[]} nodeHashes - 要 lock 的节点 hash 列表
|
|
126
|
+
* @returns {Promise<void>}
|
|
127
|
+
*/
|
|
128
|
+
export function lockReputationMax(nodeHashes) {
|
|
129
|
+
const config = loadSyncConfig()
|
|
130
|
+
const hashes = normalizeHashList(nodeHashes)
|
|
131
|
+
return mutateReputation(data => {
|
|
132
|
+
data.byNodeHash = data.byNodeHash || {}
|
|
133
|
+
for (const hash of hashes) {
|
|
134
|
+
if (config.lockedMaxNodeHashes.includes(hash)) continue
|
|
135
|
+
const prev = Number(data.byNodeHash[hash]?.score)
|
|
136
|
+
config.lockedMaxPrevByNodeHash[hash] = Number.isFinite(prev) ? prev : 0
|
|
137
|
+
config.lockedMaxNodeHashes.push(hash)
|
|
138
|
+
}
|
|
139
|
+
persistSyncConfig()
|
|
140
|
+
applyLocksToReputation(data)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 解除上限钳;还原 lock 前记下的分数。
|
|
146
|
+
* @param {string[]} nodeHashes - 要 unlock 的节点 hash 列表
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
export function unlockReputationMax(nodeHashes) {
|
|
150
|
+
const config = loadSyncConfig()
|
|
151
|
+
const remove = new Set(normalizeHashList(nodeHashes))
|
|
152
|
+
if (!remove.size) return Promise.resolve()
|
|
153
|
+
config.lockedMaxNodeHashes = config.lockedMaxNodeHashes.filter(id => !remove.has(id))
|
|
154
|
+
/** @type {Record<string, number>} */
|
|
155
|
+
const restore = {}
|
|
156
|
+
for (const hash of remove)
|
|
157
|
+
if (Object.hasOwn(config.lockedMaxPrevByNodeHash, hash)) {
|
|
158
|
+
restore[hash] = config.lockedMaxPrevByNodeHash[hash]
|
|
159
|
+
delete config.lockedMaxPrevByNodeHash[hash]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
persistSyncConfig()
|
|
163
|
+
return mutateReputation(data => {
|
|
164
|
+
data.byNodeHash = data.byNodeHash || {}
|
|
165
|
+
for (const [hash, score] of Object.entries(restore))
|
|
166
|
+
if (!data.byNodeHash[hash]) data.byNodeHash[hash] = { score }
|
|
167
|
+
else data.byNodeHash[hash].score = score
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @returns {string[]} 当前锁定为满分的节点 hash 列表
|
|
173
|
+
*/
|
|
174
|
+
export function getReputationLocks() {
|
|
175
|
+
return [...loadSyncConfig().lockedMaxNodeHashes]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {string[]} donors - 允许拉取信誉的 donor 节点
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
export function setTrustSyncDonors(donors) {
|
|
183
|
+
loadSyncConfig().trustSyncDonors = normalizeHashList(donors)
|
|
184
|
+
persistSyncConfig()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @returns {string[]} 当前 trustSyncDonors 副本
|
|
189
|
+
*/
|
|
190
|
+
export function getTrustSyncDonors() {
|
|
191
|
+
return [...loadSyncConfig().trustSyncDonors]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {string[]} allowlist - 允许导出本机信誉表的节点
|
|
196
|
+
* @returns {void}
|
|
197
|
+
*/
|
|
198
|
+
export function setReputationExportAllowlist(allowlist) {
|
|
199
|
+
loadSyncConfig().reputationExportAllowlist = normalizeHashList(allowlist)
|
|
200
|
+
persistSyncConfig()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @returns {string[]} 当前 reputationExportAllowlist 副本
|
|
205
|
+
*/
|
|
206
|
+
export function getReputationExportAllowlist() {
|
|
207
|
+
return [...loadSyncConfig().reputationExportAllowlist]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @returns {object} 仅含 score 的导出表
|
|
212
|
+
*/
|
|
213
|
+
function exportScoreTable() {
|
|
214
|
+
const rep = loadReputation()
|
|
215
|
+
/** @type {Record<string, { score: number }>} */
|
|
216
|
+
const byNodeHash = {}
|
|
217
|
+
for (const [nodeHash, row] of Object.entries(rep.byNodeHash || {}))
|
|
218
|
+
byNodeHash[nodeHash] = { score: Number(row?.score ?? 0) }
|
|
219
|
+
return { byNodeHash }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 挂载信誉同步 wire(refcount;处理 rep_sync_req / rep_sync_res)。
|
|
224
|
+
* @returns {() => void} 取消 wire 挂载的 dispose
|
|
225
|
+
*/
|
|
226
|
+
export function attachReputationSyncWire() {
|
|
227
|
+
ensureNodeScope()
|
|
228
|
+
if (!getNodeScopeWire()) throw new Error('p2p: attachReputationSyncWire requires node scope wire')
|
|
229
|
+
const dispose = attachNodeScopeFeature('rep_sync', wire => {
|
|
230
|
+
const offs = [
|
|
231
|
+
wire.on('rep_sync_req', (payload, peerId) => {
|
|
232
|
+
const requester = normalizeHex64(peerId)
|
|
233
|
+
if (!requester || !getReputationExportAllowlist().includes(requester)) return
|
|
234
|
+
try {
|
|
235
|
+
wire.send('rep_sync_res', {
|
|
236
|
+
requestId: payload?.requestId,
|
|
237
|
+
...exportScoreTable(),
|
|
238
|
+
}, peerId)
|
|
239
|
+
}
|
|
240
|
+
catch { /* disconnected */ }
|
|
241
|
+
}),
|
|
242
|
+
wire.on('rep_sync_res', (payload, peerId) => {
|
|
243
|
+
const requestId = String(payload?.requestId || '')
|
|
244
|
+
const pending = pendingPulls.get(requestId)
|
|
245
|
+
if (!pending) return
|
|
246
|
+
if (normalizeHex64(peerId) !== pending.donor) return
|
|
247
|
+
clearTimeout(pending.timer)
|
|
248
|
+
pendingPulls.delete(requestId)
|
|
249
|
+
pending.resolve(payload)
|
|
250
|
+
}),
|
|
251
|
+
]
|
|
252
|
+
return () => {
|
|
253
|
+
for (const off of offs)
|
|
254
|
+
try { off?.() } catch { /* ignore */ }
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
syncWireDisposers.add(dispose)
|
|
258
|
+
return () => {
|
|
259
|
+
if (!syncWireDisposers.delete(dispose)) return
|
|
260
|
+
dispose()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 卸掉信誉同步 wire(强制清掉全部 ref)。
|
|
266
|
+
* @returns {void}
|
|
267
|
+
*/
|
|
268
|
+
export function detachReputationSyncWire() {
|
|
269
|
+
for (const dispose of [...syncWireDisposers]) {
|
|
270
|
+
syncWireDisposers.delete(dispose)
|
|
271
|
+
try { dispose() } catch { /* ignore */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 从 donor 拉取信誉表 JSON;不落盘。应用需自行 `setReputationTable`。
|
|
277
|
+
* @param {string} nodeHash - donor 节点 64-hex hash
|
|
278
|
+
* @returns {Promise<object>} donor 返回的信誉表
|
|
279
|
+
*/
|
|
280
|
+
export async function pullReputationFromNode(nodeHash) {
|
|
281
|
+
const donor = normalizeHex64(nodeHash)
|
|
282
|
+
if (!donor || !isHex64(donor)) throw new Error('p2p: pullReputationFromNode requires valid nodeHash')
|
|
283
|
+
if (!getTrustSyncDonors().includes(donor))
|
|
284
|
+
throw new Error('p2p: node not in trustSyncDonors')
|
|
285
|
+
attachReputationSyncWire()
|
|
286
|
+
const requestId = randomUUID()
|
|
287
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
288
|
+
const timer = setTimeout(() => {
|
|
289
|
+
pendingPulls.delete(requestId)
|
|
290
|
+
reject(new Error('p2p: rep_sync timeout'))
|
|
291
|
+
}, pullTimeoutMs)
|
|
292
|
+
pendingPulls.set(requestId, { resolve, reject, timer, donor })
|
|
293
|
+
})
|
|
294
|
+
const ok = await sendToNodeLink(donor, {
|
|
295
|
+
scope: 'node',
|
|
296
|
+
action: 'rep_sync_req',
|
|
297
|
+
payload: { requestId },
|
|
298
|
+
})
|
|
299
|
+
if (!ok) {
|
|
300
|
+
const pending = pendingPulls.get(requestId)
|
|
301
|
+
if (pending) {
|
|
302
|
+
clearTimeout(pending.timer)
|
|
303
|
+
pendingPulls.delete(requestId)
|
|
304
|
+
}
|
|
305
|
+
throw new Error('p2p: rep_sync_req send failed')
|
|
306
|
+
}
|
|
307
|
+
return await resultPromise
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @returns {void}
|
|
312
|
+
*/
|
|
313
|
+
export function resetReputationSyncForTests() {
|
|
314
|
+
syncConfig = null
|
|
315
|
+
detachReputationSyncWire()
|
|
316
|
+
pendingPulls.clear()
|
|
317
|
+
pullTimeoutMs = 8000
|
|
318
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getNodeTransportSettings, saveNodeTransportSettings } from './identity.mjs'
|
|
2
|
+
|
|
3
|
+
/** @typedef {'default' | 'low'} RoutingProfile */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {RoutingProfile} profile - `default` 或 `low`(省电)
|
|
7
|
+
* @returns {RoutingProfile} 写入后的当前 profile
|
|
8
|
+
*/
|
|
9
|
+
export function setRoutingProfile(profile) {
|
|
10
|
+
if (profile !== 'default' && profile !== 'low')
|
|
11
|
+
throw new Error('p2p: setRoutingProfile expects default|low')
|
|
12
|
+
saveNodeTransportSettings({ batterySaver: profile === 'low' })
|
|
13
|
+
return getRoutingProfile()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {RoutingProfile} 当前路由 profile
|
|
18
|
+
*/
|
|
19
|
+
export function getRoutingProfile() {
|
|
20
|
+
return getNodeTransportSettings().batterySaver ? 'low' : 'default'
|
|
21
|
+
}
|
|
@@ -1,32 +1,51 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
|
|
3
|
-
/** @typedef {'none' | 'rewrite-loopback' | 'drop'}
|
|
3
|
+
/** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {{
|
|
7
7
|
* relayOverride: string[] | null
|
|
8
|
-
*
|
|
8
|
+
* iceLocalHostnamePolicy: IceLocalHostnamePolicy
|
|
9
9
|
* trickleIceOff: boolean
|
|
10
10
|
* }} SignalingRuntimeConfig
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
const ICE_LOCAL_HOSTNAME_POLICIES = new Set(['none', 'rewrite-loopback', 'drop'])
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
|
-
* 生产默认:win32 丢弃
|
|
15
|
-
* @returns {SignalingRuntimeConfig}
|
|
16
|
+
* 生产默认:win32 丢弃 `.local` host candidate;其它平台不过滤。
|
|
17
|
+
* @returns {SignalingRuntimeConfig} 默认信令运行时配置
|
|
16
18
|
*/
|
|
17
19
|
export function defaultSignalingRuntimeConfig() {
|
|
18
|
-
const
|
|
20
|
+
const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
|
|
19
21
|
return {
|
|
20
22
|
relayOverride: null,
|
|
21
|
-
|
|
22
|
-
trickleIceOff:
|
|
23
|
+
iceLocalHostnamePolicy,
|
|
24
|
+
trickleIceOff: iceLocalHostnamePolicy !== 'none',
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @returns {SignalingRuntimeConfig}
|
|
29
|
+
* @param {Partial<SignalingRuntimeConfig>} [patch] 合并字段
|
|
30
|
+
* @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
|
|
29
31
|
*/
|
|
30
|
-
export function resolveSignalingRuntimeConfig() {
|
|
31
|
-
|
|
32
|
+
export function resolveSignalingRuntimeConfig(patch = {}) {
|
|
33
|
+
const base = defaultSignalingRuntimeConfig()
|
|
34
|
+
if (!patch || typeof patch !== 'object') return base
|
|
35
|
+
const policyRaw = patch.iceLocalHostnamePolicy
|
|
36
|
+
const iceLocalHostnamePolicy = ICE_LOCAL_HOSTNAME_POLICIES.has(/** @type {string} */ policyRaw)
|
|
37
|
+
? /** @type {IceLocalHostnamePolicy} */ policyRaw
|
|
38
|
+
: base.iceLocalHostnamePolicy
|
|
39
|
+
let { relayOverride } = base
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'relayOverride'))
|
|
41
|
+
relayOverride = patch.relayOverride == null
|
|
42
|
+
? null
|
|
43
|
+
: [...new Set((Array.isArray(patch.relayOverride) ? patch.relayOverride : [])
|
|
44
|
+
.map(url => String(url || '').trim())
|
|
45
|
+
.filter(url => url.startsWith('wss://')))]
|
|
46
|
+
return {
|
|
47
|
+
relayOverride,
|
|
48
|
+
iceLocalHostnamePolicy,
|
|
49
|
+
trickleIceOff: patch.trickleIceOff !== undefined ? !!patch.trickleIceOff : iceLocalHostnamePolicy !== 'none',
|
|
50
|
+
}
|
|
32
51
|
}
|
package/overlay/index.mjs
CHANGED
|
@@ -6,6 +6,26 @@ import { createLruMap } from '../utils/lru.mjs'
|
|
|
6
6
|
|
|
7
7
|
const ROUTE_DOMAIN = 'fount-route'
|
|
8
8
|
|
|
9
|
+
/** @type {((senderNodeHash: string, action: string) => boolean) | null} */
|
|
10
|
+
let overlayRateGate = null
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 安装 overlay 入站限速门(返回 false 则丢弃)。
|
|
14
|
+
* @param {((senderNodeHash: string, action: string) => boolean) | null} rateGate 返回 false 则丢弃
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function setOverlayRateGate(rateGate) {
|
|
18
|
+
overlayRateGate = typeof rateGate === 'function' ? rateGate : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 清除 overlay 限速门。
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
export function clearOverlayRateGate() {
|
|
26
|
+
overlayRateGate = null
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
/**
|
|
10
30
|
* 构造 overlay 路由签名用的字节序列。
|
|
11
31
|
* @param {string} reqId 路由请求 id
|
|
@@ -27,7 +47,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
27
47
|
const selfPubKey = registry.localIdentity.nodePubKey
|
|
28
48
|
const { secretKey } = registry.localIdentity
|
|
29
49
|
const seenReqs = createLruMap(4096)
|
|
30
|
-
/** @type {Map<string, { resolve: (path: string[]) => void, reject: (
|
|
50
|
+
/** @type {Map<string, { resolve: (path: string[]) => void, reject: (error: Error) => void, timer: number }>} */
|
|
31
51
|
const pendingRoutes = new Map()
|
|
32
52
|
/** @type {Set<(body: unknown, meta: { path: string[], from: string }) => void>} */
|
|
33
53
|
const relayListeners = new Set()
|
|
@@ -52,6 +72,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
52
72
|
const payload = envelope?.payload
|
|
53
73
|
const action = envelope?.action || ''
|
|
54
74
|
if (!payload) return
|
|
75
|
+
if (overlayRateGate && !overlayRateGate(senderNodeHash, action)) return
|
|
55
76
|
if (action === 'route_req') {
|
|
56
77
|
const reqId = payload.reqId || ''
|
|
57
78
|
const target = payload.target || ''
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -30,8 +30,13 @@
|
|
|
30
30
|
"!sim/**",
|
|
31
31
|
"!scripts/**"
|
|
32
32
|
],
|
|
33
|
+
"bin": {
|
|
34
|
+
"fount-p2p": "./infra/cli.mjs"
|
|
35
|
+
},
|
|
33
36
|
"exports": {
|
|
34
37
|
".": "./index.mjs",
|
|
38
|
+
"./infra": "./infra/service.mjs",
|
|
39
|
+
"./link": "./link/providers/index.mjs",
|
|
35
40
|
"./core/*": "./core/*.mjs",
|
|
36
41
|
"./crypto": "./crypto/crypto.mjs",
|
|
37
42
|
"./crypto/*": "./crypto/*.mjs",
|
|
@@ -42,7 +47,12 @@
|
|
|
42
47
|
"./discovery/bt": "./discovery/bt/index.mjs",
|
|
43
48
|
"./discovery/bt/*": "./discovery/bt/*.mjs",
|
|
44
49
|
"./discovery/*": "./discovery/*.mjs",
|
|
45
|
-
"./transport
|
|
50
|
+
"./transport/link_registry": "./transport/link_registry.mjs",
|
|
51
|
+
"./transport/user_room": "./transport/user_room.mjs",
|
|
52
|
+
"./transport/group_link_set": "./transport/group_link_set.mjs",
|
|
53
|
+
"./transport/node_scope": "./transport/node_scope.mjs",
|
|
54
|
+
"./transport/room_scopes": "./transport/room_scopes.mjs",
|
|
55
|
+
"./transport/remote_user_room": "./transport/remote_user_room.mjs",
|
|
46
56
|
"./rooms/*": "./rooms/*.mjs",
|
|
47
57
|
"./overlay": "./overlay/index.mjs",
|
|
48
58
|
"./overlay/*": "./overlay/*.mjs",
|
|
@@ -62,6 +72,7 @@
|
|
|
62
72
|
"dependencies": {
|
|
63
73
|
"@noble/curves": "latest",
|
|
64
74
|
"node-datachannel": "latest",
|
|
75
|
+
"on-shutdown": "latest",
|
|
65
76
|
"ws": "latest"
|
|
66
77
|
},
|
|
67
78
|
"devDependencies": {
|
package/rooms/scoped_link.mjs
CHANGED
|
@@ -1,171 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ingestSignedAdvert } from '../transport/advert_ingest.mjs'
|
|
3
|
-
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
4
|
-
import {
|
|
5
|
-
encryptSignalPacket,
|
|
6
|
-
groupRendezvousTopic,
|
|
7
|
-
} from '../transport/signal_crypto.mjs'
|
|
8
|
-
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
1
|
+
import { createGroupLinkSet } from '../transport/group_link_set.mjs'
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {object} options 房间选项
|
|
13
|
-
* @param {string} options.scope link registry scope
|
|
14
|
-
* @param {string} options.roomSecret
|
|
15
|
-
* @param {(nodeHash: string) => boolean} [options.allowNode]
|
|
16
|
-
* @returns {
|
|
4
|
+
* scoped 房间:group_link_set 的薄预设(任意 scope + allowNode + 发现即拨)。
|
|
5
|
+
* @param {object} options - 房间选项
|
|
6
|
+
* @param {string} options.scope - link registry scope
|
|
7
|
+
* @param {string} options.roomSecret - rendezvous 密钥
|
|
8
|
+
* @param {(nodeHash: string) => boolean} [options.allowNode] - 节点准入过滤
|
|
9
|
+
* @returns {ReturnType<typeof createGroupLinkSet>} scoped link 房间句柄
|
|
17
10
|
*/
|
|
18
11
|
export function createScopedLinkRoom(options) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** @type {Set<(peerId: string) => void>} */
|
|
30
|
-
const leaveListeners = new Set()
|
|
31
|
-
/** @type {Set<() => void>} */
|
|
32
|
-
const cleanups = new Set()
|
|
33
|
-
/** @type {Map<string, { handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }>} */
|
|
34
|
-
const actionEntries = new Map()
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @returns {string[]} 当前已连接且通过 allowNode 过滤的 peer nodeHash 列表
|
|
38
|
-
*/
|
|
39
|
-
function activePeerIds() {
|
|
40
|
-
return [...discoveredPeers].filter(nodeHash => allowNode(nodeHash) && registry.getLink(nodeHash))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {string} peerId 节点 hash
|
|
45
|
-
* @returns {void}
|
|
46
|
-
*/
|
|
47
|
-
function notePeerJoin(peerId) {
|
|
48
|
-
if (!peerId || announcedPeers.has(peerId)) return
|
|
49
|
-
announcedPeers.add(peerId)
|
|
50
|
-
emitSafe(joinListeners, peerId)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {string} peerId 节点 hash
|
|
55
|
-
* @returns {void}
|
|
56
|
-
*/
|
|
57
|
-
function notePeerLeave(peerId) {
|
|
58
|
-
if (!peerId || !announcedPeers.has(peerId)) return
|
|
59
|
-
announcedPeers.delete(peerId)
|
|
60
|
-
emitSafe(leaveListeners, peerId)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @param {string} name action 名称
|
|
65
|
-
* @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 槽(含待处理 backlog)
|
|
66
|
-
*/
|
|
67
|
-
function getActionEntry(name) {
|
|
68
|
-
if (!actionEntries.has(name))
|
|
69
|
-
actionEntries.set(name, { handler: null, backlog: [] })
|
|
70
|
-
return actionEntries.get(name)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
/**
|
|
75
|
-
* @returns {Promise<void>}
|
|
76
|
-
*/
|
|
77
|
-
async start() {
|
|
78
|
-
await registry.ensureRuntime()
|
|
79
|
-
cleanups.add(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
|
|
80
|
-
if (!allowNode(senderNodeHash)) return
|
|
81
|
-
const entry = actionEntries.get(envelope.action)
|
|
82
|
-
if (!entry) return
|
|
83
|
-
if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
|
|
84
|
-
else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
|
|
85
|
-
}))
|
|
86
|
-
cleanups.add(registry.onLinkUp(nodeHash => {
|
|
87
|
-
if (!discoveredPeers.has(nodeHash) || !allowNode(nodeHash)) return
|
|
88
|
-
notePeerJoin(nodeHash)
|
|
89
|
-
}))
|
|
90
|
-
cleanups.add(registry.onLinkDown(nodeHash => {
|
|
91
|
-
if (!discoveredPeers.has(nodeHash)) return
|
|
92
|
-
notePeerLeave(nodeHash)
|
|
93
|
-
}))
|
|
94
|
-
cleanups.add(await subscribeTopic(topic, async (bytes, meta) => {
|
|
95
|
-
const ingested = await ingestSignedAdvert(topic, bytes, meta)
|
|
96
|
-
if (!ingested || !allowNode(ingested.verifiedNodeHash)) return
|
|
97
|
-
discoveredPeers.add(ingested.verifiedNodeHash)
|
|
98
|
-
await registry.ensureLinkToNode(ingested.verifiedNodeHash).catch(() => null)
|
|
99
|
-
if (registry.getLink(ingested.verifiedNodeHash)) notePeerJoin(ingested.verifiedNodeHash)
|
|
100
|
-
}))
|
|
101
|
-
cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
102
|
-
type: 'advert',
|
|
103
|
-
body: await registry.buildLocalAdvert(topic),
|
|
104
|
-
})))
|
|
105
|
-
for (const peerId of activePeerIds())
|
|
106
|
-
notePeerJoin(peerId)
|
|
107
|
-
},
|
|
108
|
-
/**
|
|
109
|
-
* @returns {Promise<void>}
|
|
110
|
-
*/
|
|
111
|
-
async leave() {
|
|
112
|
-
for (const cleanup of cleanups)
|
|
113
|
-
try { cleanup() } catch { /* ignore */ }
|
|
114
|
-
cleanups.clear()
|
|
115
|
-
for (const peerId of [...announcedPeers])
|
|
116
|
-
notePeerLeave(peerId)
|
|
117
|
-
},
|
|
118
|
-
/**
|
|
119
|
-
* @param {string} name action 名称
|
|
120
|
-
* @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} [send, onReceive] 发送与订阅元组
|
|
121
|
-
*/
|
|
122
|
-
makeAction(name) {
|
|
123
|
-
return [
|
|
124
|
-
async (payload, peerId = null) => {
|
|
125
|
-
if (Array.isArray(peerId)) {
|
|
126
|
-
await Promise.all(peerId.map(targetPeerId =>
|
|
127
|
-
registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
if (peerId)
|
|
131
|
-
await registry.sendToNodeLink(peerId, { scope, action: name, payload })
|
|
132
|
-
else
|
|
133
|
-
await Promise.all(activePeerIds().map(targetPeerId =>
|
|
134
|
-
registry.sendToNodeLink(targetPeerId, { scope, action: name, payload })))
|
|
135
|
-
},
|
|
136
|
-
handler => {
|
|
137
|
-
const entry = getActionEntry(name)
|
|
138
|
-
entry.handler = handler
|
|
139
|
-
for (const pending of entry.backlog.splice(0))
|
|
140
|
-
handler(pending.payload, pending.peerId)
|
|
141
|
-
},
|
|
142
|
-
]
|
|
143
|
-
},
|
|
144
|
-
/**
|
|
145
|
-
* @param {(peerId: string) => void} callback 新 peer 上线回调
|
|
146
|
-
* @returns {() => void} 取消订阅
|
|
147
|
-
*/
|
|
148
|
-
onPeerJoin(callback) {
|
|
149
|
-
joinListeners.add(callback)
|
|
150
|
-
for (const peerId of activePeerIds())
|
|
151
|
-
announcedPeers.add(peerId)
|
|
152
|
-
for (const peerId of announcedPeers)
|
|
153
|
-
try { callback(peerId) } catch { /* ignore */ }
|
|
154
|
-
return () => joinListeners.delete(callback)
|
|
155
|
-
},
|
|
156
|
-
/**
|
|
157
|
-
* @param {(peerId: string) => void} callback peer 离线回调
|
|
158
|
-
* @returns {() => void} 取消订阅
|
|
159
|
-
*/
|
|
160
|
-
onPeerLeave(callback) {
|
|
161
|
-
leaveListeners.add(callback)
|
|
162
|
-
return () => leaveListeners.delete(callback)
|
|
163
|
-
},
|
|
164
|
-
/**
|
|
165
|
-
* @returns {Record<string, true>} 当前活跃 peer 的 nodeHash 集合
|
|
166
|
-
*/
|
|
167
|
-
getPeers() {
|
|
168
|
-
return Object.fromEntries(activePeerIds().map(peerId => [peerId, true]))
|
|
169
|
-
},
|
|
170
|
-
}
|
|
12
|
+
const { scope, roomSecret, allowNode } = options
|
|
13
|
+
return createGroupLinkSet({
|
|
14
|
+
groupId: scope,
|
|
15
|
+
scope,
|
|
16
|
+
roomSecret,
|
|
17
|
+
members: [],
|
|
18
|
+
allowNode,
|
|
19
|
+
dialAll: true,
|
|
20
|
+
autoconnect: true,
|
|
21
|
+
})
|
|
171
22
|
}
|