@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.
Files changed (75) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +1 -1
  7. package/discovery/adverts.mjs +109 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
@@ -1,32 +1,51 @@
1
1
  import process from 'node:process'
2
2
 
3
- /** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
3
+ /** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
4
4
 
5
5
  /**
6
6
  * @typedef {{
7
7
  * relayOverride: string[] | null
8
- * mdnsPolicy: MdnsCandidatePolicy
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 丢弃 .local host candidate;其它平台不过滤。
15
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
16
+ * 生产默认:win32 丢弃 `.local` host candidate;其它平台不过滤。
17
+ * @returns {SignalingRuntimeConfig} 默认信令运行时配置
16
18
  */
17
19
  export function defaultSignalingRuntimeConfig() {
18
- const mdnsPolicy = process.platform === 'win32' ? 'drop' : 'none'
20
+ const iceLocalHostnamePolicy = process.platform === 'win32' ? 'drop' : 'none'
19
21
  return {
20
22
  relayOverride: null,
21
- mdnsPolicy,
22
- trickleIceOff: mdnsPolicy !== 'none',
23
+ iceLocalHostnamePolicy,
24
+ trickleIceOff: iceLocalHostnamePolicy !== 'none',
23
25
  }
24
26
  }
25
27
 
26
28
  /**
27
- * 生产默认信令配置;测试通过 initNode({ signaling }) 注入。
28
- * @returns {SignalingRuntimeConfig} 生产默认信令配置
29
+ * @param {Partial<SignalingRuntimeConfig>} [patch] 合并字段
30
+ * @returns {SignalingRuntimeConfig} 合并后的信令运行时配置
29
31
  */
30
- export function resolveSignalingRuntimeConfig() {
31
- return defaultSignalingRuntimeConfig()
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: (err: Error) => void, timer: number }>} */
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.12",
3
+ "version": "0.0.13",
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/*": "./transport/*.mjs",
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": {
@@ -1,171 +1,22 @@
1
- import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
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
- * 在指定 scope roomSecret 下创建 link 层房间(discovery + registry 转发)。
12
- * @param {object} options 房间选项
13
- * @param {string} options.scope link registry scope(如 `group:{id}`)
14
- * @param {string} options.roomSecret 房间 rendezvous 密钥
15
- * @param {(nodeHash: string) => boolean} [options.allowNode] 是否允许与某 nodeHash 通信
16
- * @returns {{ start: () => Promise<void>, leave: () => Promise<void>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], onPeerJoin: (callback: (peerId: string) => void) => () => void, onPeerLeave: (callback: (peerId: string) => void) => () => void, getPeers: () => Record<string, true> }} 房间句柄
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 registry = getLinkRegistry()
20
- const { scope } = options
21
- const topic = groupRendezvousTopic(options.roomSecret)
22
- const allowNode = options.allowNode ?? (() => true)
23
- /** @type {Set<string>} */
24
- const discoveredPeers = new Set()
25
- /** @type {Set<string>} */
26
- const announcedPeers = new Set()
27
- /** @type {Set<(peerId: string) => void>} */
28
- const joinListeners = new Set()
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
  }
@@ -1,20 +1,17 @@
1
1
  import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
2
- import { verifySignedAdvert } from '../link/handshake.mjs'
3
2
 
4
- import { decryptSignalPacket } from './signal_crypto.mjs'
3
+ /**
4
+ * 解密并验签 advert(`ingestEncryptedAdvert` 别名)。不写 peer hints;写入需 `applyAdvertPeerHints`。Untrusted ingress。
5
+ */
6
+ export { ingestEncryptedAdvert as ingestSignedAdvert } from '../discovery/adverts.mjs'
5
7
 
6
8
  /**
7
- * 解密验签 advert,写入 peer hints;失败返回 null。
8
- * @param {string} topic rendezvous 主题
9
- * @param {Uint8Array} bytes 加密 advert 载荷
10
- * @param {object} [meta] 发现元数据(如 relayUrl)
11
- * @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签后的节点与 body
9
+ * 将已验签 advert peer hints 写入本地(apply 侧;与 `ingestSignedAdvert` 分离)。
10
+ * @param {string} verifiedNodeHash 已验签 nodeHash
11
+ * @param {object} body advert body
12
+ * @param {object} [meta] 元数据
13
+ * @returns {void}
12
14
  */
13
- export async function ingestSignedAdvert(topic, bytes, meta) {
14
- const packet = decryptSignalPacket(topic, bytes)
15
- if (packet?.type !== 'advert' || !packet.body) return null
16
- const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
17
- if (!verifiedNodeHash) return null
18
- noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
19
- return { verifiedNodeHash, body: packet.body }
15
+ export function applyAdvertPeerHints(verifiedNodeHash, body, meta) {
16
+ noteAdvertPeerHints(verifiedNodeHash, body, meta)
20
17
  }