@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.
- package/README.md +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- 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 +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- 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/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- 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 +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- 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 +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
package/rooms/scoped_link.mjs
CHANGED
|
@@ -1,187 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
3
|
-
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
4
|
-
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
|
-
import {
|
|
6
|
-
decryptSignalPacket,
|
|
7
|
-
encryptSignalPacket,
|
|
8
|
-
groupRendezvousTopic,
|
|
9
|
-
} from '../transport/signal_crypto.mjs'
|
|
1
|
+
import { createGroupLinkSet } from '../transport/group_link_set.mjs'
|
|
10
2
|
|
|
11
3
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param {object} options 房间选项
|
|
14
|
-
* @param {string} options.scope link registry scope
|
|
15
|
-
* @param {string} options.roomSecret
|
|
16
|
-
* @param {(nodeHash: string) => boolean} [options.allowNode]
|
|
17
|
-
* @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 房间句柄
|
|
18
10
|
*/
|
|
19
11
|
export function createScopedLinkRoom(options) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** @type {Set<(peerId: string) => void>} */
|
|
31
|
-
const leaveListeners = new Set()
|
|
32
|
-
/** @type {Set<() => void>} */
|
|
33
|
-
const cleanups = new Set()
|
|
34
|
-
/** @type {Map<string, { handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }>} */
|
|
35
|
-
const actionEntries = new Map()
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @returns {string[]} 当前已连接且通过 allowNode 过滤的 peer nodeHash 列表
|
|
39
|
-
*/
|
|
40
|
-
function activePeerIds() {
|
|
41
|
-
return [...discoveredPeers].filter(nodeHash => allowNode(nodeHash) && registry.getLink(nodeHash))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {Set<(peerId: string) => void>} listeners 回调集合
|
|
46
|
-
* @param {string} peerId 节点 hash
|
|
47
|
-
* @returns {void}
|
|
48
|
-
*/
|
|
49
|
-
function emit(listeners, peerId) {
|
|
50
|
-
for (const listener of listeners)
|
|
51
|
-
try { listener(peerId) } catch { /* ignore */ }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @param {string} peerId 节点 hash
|
|
56
|
-
* @returns {void}
|
|
57
|
-
*/
|
|
58
|
-
function notePeerJoin(peerId) {
|
|
59
|
-
if (!peerId || announcedPeers.has(peerId)) return
|
|
60
|
-
announcedPeers.add(peerId)
|
|
61
|
-
emit(joinListeners, peerId)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @param {string} peerId 节点 hash
|
|
66
|
-
* @returns {void}
|
|
67
|
-
*/
|
|
68
|
-
function notePeerLeave(peerId) {
|
|
69
|
-
if (!peerId || !announcedPeers.has(peerId)) return
|
|
70
|
-
announcedPeers.delete(peerId)
|
|
71
|
-
emit(leaveListeners, peerId)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param {string} name action 名称
|
|
76
|
-
* @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 槽(含待处理 backlog)
|
|
77
|
-
*/
|
|
78
|
-
function getActionEntry(name) {
|
|
79
|
-
const key = String(name)
|
|
80
|
-
if (!actionEntries.has(key))
|
|
81
|
-
actionEntries.set(key, { handler: null, backlog: [] })
|
|
82
|
-
return actionEntries.get(key)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
/**
|
|
87
|
-
* @returns {Promise<void>}
|
|
88
|
-
*/
|
|
89
|
-
async start() {
|
|
90
|
-
await registry.ensureRuntime()
|
|
91
|
-
cleanups.add(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
|
|
92
|
-
if (!allowNode(senderNodeHash)) return
|
|
93
|
-
const entry = actionEntries.get(String(envelope?.action || ''))
|
|
94
|
-
if (!entry) return
|
|
95
|
-
if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
|
|
96
|
-
else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
|
|
97
|
-
}))
|
|
98
|
-
cleanups.add(registry.onLinkUp(nodeHash => {
|
|
99
|
-
if (!discoveredPeers.has(nodeHash) || !allowNode(nodeHash)) return
|
|
100
|
-
notePeerJoin(nodeHash)
|
|
101
|
-
}))
|
|
102
|
-
cleanups.add(registry.onLinkDown(nodeHash => {
|
|
103
|
-
if (!discoveredPeers.has(nodeHash)) return
|
|
104
|
-
notePeerLeave(nodeHash)
|
|
105
|
-
}))
|
|
106
|
-
cleanups.add(await subscribeTopic(topic, async (bytes, meta) => {
|
|
107
|
-
const packet = decryptSignalPacket(topic, bytes)
|
|
108
|
-
if (packet?.type !== 'advert' || !packet.body) return
|
|
109
|
-
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
110
|
-
if (!verifiedNodeHash || !allowNode(verifiedNodeHash)) return
|
|
111
|
-
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
112
|
-
discoveredPeers.add(verifiedNodeHash)
|
|
113
|
-
await registry.ensureLinkToNode(verifiedNodeHash).catch(() => null)
|
|
114
|
-
if (registry.getLink(verifiedNodeHash)) notePeerJoin(verifiedNodeHash)
|
|
115
|
-
}))
|
|
116
|
-
cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
117
|
-
type: 'advert',
|
|
118
|
-
body: await registry.buildLocalAdvert(topic),
|
|
119
|
-
})))
|
|
120
|
-
for (const peerId of activePeerIds())
|
|
121
|
-
notePeerJoin(peerId)
|
|
122
|
-
},
|
|
123
|
-
/**
|
|
124
|
-
* @returns {Promise<void>}
|
|
125
|
-
*/
|
|
126
|
-
async leave() {
|
|
127
|
-
for (const cleanup of cleanups)
|
|
128
|
-
try { cleanup() } catch { /* ignore */ }
|
|
129
|
-
cleanups.clear()
|
|
130
|
-
for (const peerId of [...announcedPeers])
|
|
131
|
-
notePeerLeave(peerId)
|
|
132
|
-
},
|
|
133
|
-
/**
|
|
134
|
-
* @param {string} name action 名称
|
|
135
|
-
* @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} [send, onReceive] 发送与订阅元组
|
|
136
|
-
*/
|
|
137
|
-
makeAction(name) {
|
|
138
|
-
const actionName = String(name)
|
|
139
|
-
return [
|
|
140
|
-
async (payload, peerId = null) => {
|
|
141
|
-
if (Array.isArray(peerId)) {
|
|
142
|
-
await Promise.all(peerId.map(targetPeerId =>
|
|
143
|
-
registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })))
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
if (peerId)
|
|
147
|
-
await registry.sendToNodeLink(peerId, { scope, action: actionName, payload })
|
|
148
|
-
else
|
|
149
|
-
await Promise.all(activePeerIds().map(targetPeerId =>
|
|
150
|
-
registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })))
|
|
151
|
-
},
|
|
152
|
-
handler => {
|
|
153
|
-
const entry = getActionEntry(actionName)
|
|
154
|
-
entry.handler = handler
|
|
155
|
-
for (const pending of entry.backlog.splice(0))
|
|
156
|
-
handler(pending.payload, pending.peerId)
|
|
157
|
-
},
|
|
158
|
-
]
|
|
159
|
-
},
|
|
160
|
-
/**
|
|
161
|
-
* @param {(peerId: string) => void} callback 新 peer 上线回调
|
|
162
|
-
* @returns {() => void} 取消订阅
|
|
163
|
-
*/
|
|
164
|
-
onPeerJoin(callback) {
|
|
165
|
-
joinListeners.add(callback)
|
|
166
|
-
for (const peerId of activePeerIds())
|
|
167
|
-
announcedPeers.add(peerId)
|
|
168
|
-
for (const peerId of announcedPeers)
|
|
169
|
-
try { callback(peerId) } catch { /* ignore */ }
|
|
170
|
-
return () => joinListeners.delete(callback)
|
|
171
|
-
},
|
|
172
|
-
/**
|
|
173
|
-
* @param {(peerId: string) => void} callback peer 离线回调
|
|
174
|
-
* @returns {() => void} 取消订阅
|
|
175
|
-
*/
|
|
176
|
-
onPeerLeave(callback) {
|
|
177
|
-
leaveListeners.add(callback)
|
|
178
|
-
return () => leaveListeners.delete(callback)
|
|
179
|
-
},
|
|
180
|
-
/**
|
|
181
|
-
* @returns {Record<string, true>} 当前活跃 peer 的 nodeHash 集合
|
|
182
|
-
*/
|
|
183
|
-
getPeers() {
|
|
184
|
-
return Object.fromEntries(activePeerIds().map(peerId => [peerId, true]))
|
|
185
|
-
},
|
|
186
|
-
}
|
|
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
|
+
})
|
|
187
22
|
}
|
package/schemas/discovery.mjs
CHANGED
|
@@ -34,8 +34,7 @@ function sanitizeDiscoveryAdvertisement(ad) {
|
|
|
34
34
|
* @returns {object[]} 已清扫广告
|
|
35
35
|
*/
|
|
36
36
|
function sanitizeDiscoveryAdvertisements(ads) {
|
|
37
|
-
|
|
38
|
-
return ads.slice(0, MAX_DISCOVERY_ADS).map(sanitizeDiscoveryAdvertisement).filter(Boolean)
|
|
37
|
+
return (ads || []).slice(0, MAX_DISCOVERY_ADS).map(sanitizeDiscoveryAdvertisement).filter(Boolean)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
/**
|
|
@@ -30,8 +30,8 @@ export function parsePullAttestation(attestation) {
|
|
|
30
30
|
const signature = String(attestation.signature || '').trim().toLowerCase()
|
|
31
31
|
if (!isHex64(requesterPubKeyHash) || !groupId || !Number.isFinite(timestamp) || !isSignatureHex128(signature))
|
|
32
32
|
return null
|
|
33
|
-
const wantIds =
|
|
34
|
-
? [...new Set(attestation.wantIds.map(id =>
|
|
33
|
+
const wantIds = attestation.wantIds?.length
|
|
34
|
+
? [...new Set(attestation.wantIds.map(id => id.trim().toLowerCase()).filter(id => EVENT_ID_HEX.test(id)))]
|
|
35
35
|
: undefined
|
|
36
36
|
return {
|
|
37
37
|
requesterPubKeyHash,
|
package/schemas/part_query.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export function parsePartQueryReq(value, tunables = partQueryTunables) {
|
|
|
107
107
|
if (!Object.prototype.hasOwnProperty.call(value, 'query')) return null
|
|
108
108
|
if (measureJsonBytes(value.query) > (tunables.maxQueryBytes ?? 2048)) return null
|
|
109
109
|
const ttl = clampPartQueryTtl(value.ttl, tunables.maxTtl)
|
|
110
|
-
if (ttl
|
|
110
|
+
if (!ttl) return null
|
|
111
111
|
const budget = clampPartQueryBudget(value.budget, tunables.maxHits)
|
|
112
112
|
return {
|
|
113
113
|
requestId,
|
package/schemas/remote_event.mjs
CHANGED
|
@@ -9,7 +9,7 @@ export function validateRemoteEventShape(event) {
|
|
|
9
9
|
if (!event?.type || typeof event.type !== 'string')
|
|
10
10
|
throw new Error('remote event: type required')
|
|
11
11
|
assertHex64(event.id, 'remote event.id')
|
|
12
|
-
if (!
|
|
12
|
+
if (!event.prev_event_ids)
|
|
13
13
|
throw new Error('remote event: prev_event_ids must be array')
|
|
14
14
|
for (const parentId of event.prev_event_ids)
|
|
15
15
|
assertHex64(parentId, 'remote event.prev_event_id')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 解密并验签 advert(`ingestEncryptedAdvert` 别名)。不写 peer hints;写入需 `applyAdvertPeerHints`。Untrusted ingress。
|
|
5
|
+
*/
|
|
6
|
+
export { ingestEncryptedAdvert as ingestSignedAdvert } from '../discovery/adverts.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 将已验签 advert 的 peer hints 写入本地(apply 侧;与 `ingestSignedAdvert` 分离)。
|
|
10
|
+
* @param {string} verifiedNodeHash 已验签 nodeHash
|
|
11
|
+
* @param {object} body advert body
|
|
12
|
+
* @param {object} [meta] 元数据
|
|
13
|
+
* @returns {void}
|
|
14
|
+
*/
|
|
15
|
+
export function applyAdvertPeerHints(verifiedNodeHash, body, meta) {
|
|
16
|
+
noteAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
17
|
+
}
|