@steve02081504/fount-p2p 0.0.0

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 (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,115 @@
1
+ import path from 'node:path'
2
+
3
+ import { createFsEntityStore } from './entity_store.mjs'
4
+ import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './signaling_config.mjs'
5
+
6
+ /** @typedef {{ warn?: (...args: unknown[]) => void, error?: (...args: unknown[]) => void }} NodeLogger */
7
+
8
+ /** @typedef {import('./signaling_config.mjs').SignalingRuntimeConfig} SignalingRuntimeConfig */
9
+
10
+ /**
11
+ * @typedef {{
12
+ * nodeDir: string
13
+ * entityStore: import('./entity_store.mjs').EntityStore
14
+ * logger: NodeLogger
15
+ * signaling: SignalingRuntimeConfig
16
+ * }} NodeRuntime
17
+ */
18
+
19
+ /** @type {NodeRuntime | null} */
20
+ let runtime = null
21
+
22
+ /** @type {Set<(event: string, payload?: unknown) => void>} */
23
+ const changeListeners = new Set()
24
+
25
+ /** @type {NodeLogger} */
26
+ const noopLogger = {
27
+ /**
28
+ *
29
+ */
30
+ warn() { },
31
+ /**
32
+ *
33
+ */
34
+ error() { },
35
+ }
36
+
37
+ /**
38
+ * @param {{ nodeDir: string, entityStore?: import('./entity_store.mjs').EntityStore, logger?: NodeLogger, signaling?: SignalingRuntimeConfig }} options 节点目录与可选注入
39
+ * @returns {NodeRuntime} 单节点运行时句柄
40
+ */
41
+ export function initNode(options) {
42
+ const nodeDir = path.resolve(String(options.nodeDir || '').trim())
43
+ if (!nodeDir) throw new Error('p2p: initNode requires nodeDir')
44
+ const entityStore = options.entityStore ?? createFsEntityStore(path.join(nodeDir, 'entities'))
45
+ runtime = {
46
+ nodeDir,
47
+ entityStore,
48
+ logger: options.logger ?? console,
49
+ signaling: options.signaling ?? resolveSignalingRuntimeConfig(),
50
+ }
51
+ return runtime
52
+ }
53
+
54
+ /**
55
+ * @returns {NodeRuntime} 已初始化的节点运行时
56
+ */
57
+ export function getNode() {
58
+ if (!runtime) throw new Error('p2p: node not initialized — call initNode() first')
59
+ return runtime
60
+ }
61
+
62
+ /**
63
+ * @returns {boolean} 是否已调用 initNode
64
+ */
65
+ export function isNodeInitialized() {
66
+ return runtime != null
67
+ }
68
+
69
+ /**
70
+ * @returns {SignalingRuntimeConfig} 信令传输策略(未 init 时返回生产默认,不读测试 env)
71
+ */
72
+ export function getSignalingRuntimeConfig() {
73
+ return runtime?.signaling ?? defaultSignalingRuntimeConfig()
74
+ }
75
+
76
+ /**
77
+ * @returns {string} 节点数据目录绝对路径
78
+ */
79
+ export function getNodeDir() {
80
+ return getNode().nodeDir
81
+ }
82
+
83
+ /**
84
+ * @returns {import('./entity_store.mjs').EntityStore} 注入的 EntityStore
85
+ */
86
+ export function getEntityStore() {
87
+ return getNode().entityStore
88
+ }
89
+
90
+ /**
91
+ * @returns {NodeLogger} 节点日志器(未 init 时为 no-op)
92
+ */
93
+ export function getNodeLogger() {
94
+ return runtime?.logger ?? noopLogger
95
+ }
96
+
97
+ /**
98
+ * @param {string} event 事件名
99
+ * @param {unknown} [payload] 载荷
100
+ * @returns {void}
101
+ */
102
+ export function emitNodeChange(event, payload) {
103
+ for (const listener of changeListeners)
104
+ try { listener(event, payload) }
105
+ catch { /* ignore */ }
106
+ }
107
+
108
+ /**
109
+ * @param {(event: string, payload?: unknown) => void} listener 回调
110
+ * @returns {() => void} 取消订阅
111
+ */
112
+ export function onNodeChange(listener) {
113
+ changeListeners.add(listener)
114
+ return () => changeListeners.delete(listener)
115
+ }
@@ -0,0 +1,306 @@
1
+ import { isEntityHash128 } from '../core/entity_id.mjs'
2
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
3
+ import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
4
+
5
+ import { loadDenylist } from './denylist.mjs'
6
+ import { getNodeDir, isNodeInitialized } from './instance.mjs'
7
+ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
8
+
9
+
10
+ /**
11
+ * 联邦术语(Wave 6):
12
+ * - **block**:Social 对外联邦公开拉黑(personal_block / timeline block 事件)
13
+ * - **hide**:纯本地隐藏(personal_hide,不联邦)
14
+ * - **deny**:节点连接拒绝(denylist.json,scope node/subject/entity)
15
+ * - **ban**:群成员治理(member_ban DAG + bannedMembers 物化态)
16
+ *
17
+ * @typedef {{
18
+ * trustedPeers: string[]
19
+ * explorePeers: string[]
20
+ * blockedPeers: string[]
21
+ * deniedNodes: string[]
22
+ * deniedSubjects: string[]
23
+ * deniedEntities: string[]
24
+ * lastRosterAt: number
25
+ * hintSources?: Map<string, string>
26
+ * }} PeerPoolView
27
+ */
28
+
29
+ const DATA_NAME = 'network'
30
+ const MAX_EXPLORE = 500
31
+ const MAX_HINTS = 256
32
+ const MAX_HINTS_PER_SOURCE = 12
33
+ const DEFAULT_EXPLORE_TTL_MS = 7 * 24 * 60 * 60 * 1000
34
+
35
+ /** @type {ReturnType<typeof normalizeNetwork> | null} network.json 内存缓存(唯一写路径 saveNetwork 负责刷新) */
36
+ let networkCache = null
37
+ /** @type {string | null} 缓存所属 nodeDir(切换节点/未初始化时失效) */
38
+ let networkCacheNodeDir = null
39
+
40
+ /**
41
+ * @typedef {{ nodeHash: string, source: string, kind: string, weight?: number, expiresAt: number, groupId?: string }} NetworkHint
42
+ */
43
+
44
+ /**
45
+ * @param {unknown} raw 磁盘 JSON
46
+ * @returns {{ trustedPeers: string[], explorePeers: string[], hints: NetworkHint[], lastRosterAt: number }} 规范化网络表
47
+ */
48
+ export function normalizeNetwork(raw) {
49
+ const file = raw ?? {}
50
+ /**
51
+ * @param {string} key 字段名
52
+ * @returns {string[]} 去重 nodeHash 列表
53
+ */
54
+ const pickIds = key => [...new Set(
55
+ (Array.isArray(file[key]) ? file[key] : [])
56
+ .map(id => normalizeHex64(id) || String(id).trim())
57
+ .filter(id => isHex64(id)),
58
+ )]
59
+ const hints = (Array.isArray(file.hints) ? file.hints : [])
60
+ .map(hint => ({
61
+ nodeHash: normalizeHex64(hint?.nodeHash) || '',
62
+ source: String(hint?.source || '').trim(),
63
+ kind: String(hint?.kind || '').trim(),
64
+ weight: Number.isFinite(Number(hint?.weight)) ? Number(hint.weight) : 0.1,
65
+ expiresAt: Number(hint?.expiresAt) || 0,
66
+ ...hint?.groupId ? { groupId: String(hint.groupId).trim() } : {},
67
+ }))
68
+ .filter(hint => isHex64(hint.nodeHash))
69
+ return {
70
+ trustedPeers: pickIds('trustedPeers'),
71
+ explorePeers: pickIds('explorePeers'),
72
+ hints,
73
+ lastRosterAt: Number.isFinite(file.lastRosterAt) ? Number(file.lastRosterAt) : 0,
74
+ }
75
+ }
76
+
77
+ /**
78
+ * 节点级 P2P 网络(内存缓存;热路径 loadPeerPoolView 每首见事件都会调用,避免每次同步读盘)。
79
+ * @returns {{ trustedPeers: string[], explorePeers: string[], hints: NetworkHint[], lastRosterAt: number }} 节点级 P2P 网络
80
+ */
81
+ export function loadNetwork() {
82
+ const nodeDir = isNodeInitialized() ? getNodeDir() : ''
83
+ if (networkCache && networkCacheNodeDir === nodeDir) return networkCache
84
+ networkCacheNodeDir = nodeDir
85
+ networkCache = normalizeNetwork(readNodeJsonSync(DATA_NAME))
86
+ return networkCache
87
+ }
88
+
89
+ /**
90
+ * 限制同一 source 的 hint 数量,防止 PEX/单源灌满 explore。
91
+ * @param {NetworkHint[]} hints hint 列表
92
+ * @param {number} [maxPerSource=MAX_HINTS_PER_SOURCE] 每源上限
93
+ * @returns {NetworkHint[]} 裁剪后列表(保留较新条目)
94
+ */
95
+ export function capHintsBySource(hints, maxPerSource = MAX_HINTS_PER_SOURCE) {
96
+ /** @type {Map<string, number>} */
97
+ const counts = new Map()
98
+ const out = []
99
+ for (const hint of [...hints].reverse()) {
100
+ const src = String(hint.source || 'unknown')
101
+ const n = counts.get(src) ?? 0
102
+ if (n >= maxPerSource) continue
103
+ counts.set(src, n + 1)
104
+ out.unshift(hint)
105
+ }
106
+ return out
107
+ }
108
+
109
+ /**
110
+ * @param {ReturnType<typeof normalizeNetwork>} data 网络表
111
+ * @returns {void}
112
+ */
113
+ export function saveNetwork(data) {
114
+ const clean = normalizeNetwork(data)
115
+ const now = Date.now()
116
+ clean.hints = capHintsBySource(clean.hints.filter(h => !h.expiresAt || h.expiresAt > now)).slice(-MAX_HINTS)
117
+ clean.explorePeers = clean.explorePeers.slice(-MAX_EXPLORE)
118
+ writeNodeJsonSync(DATA_NAME, clean)
119
+ networkCache = clean
120
+ networkCacheNodeDir = isNodeInitialized() ? getNodeDir() : ''
121
+ invalidateTrustGraphCache()
122
+ }
123
+
124
+ /**
125
+ * @param {string} nodeHash 64 位十六进制
126
+ * @param {'trusted' | 'explore'} tier 池档位
127
+ * @returns {void}
128
+ */
129
+ export function addNetworkPeer(nodeHash, tier = 'explore') {
130
+ const id = normalizeHex64(nodeHash)
131
+ if (!isHex64(id)) return
132
+ const net = loadNetwork()
133
+ const list = tier === 'trusted' ? net.trustedPeers : net.explorePeers
134
+ if (!list.includes(id)) list.push(id)
135
+ saveNetwork(net)
136
+ }
137
+
138
+ /**
139
+ * @param {{ nodeHash: string, source: string, kind: string, weight?: number, expiresAt?: number, ttlMs?: number, groupId?: string }} hint 扩边 hint
140
+ * @returns {void}
141
+ */
142
+ export function applyNetworkHint(hint) {
143
+ const nodeHash = normalizeHex64(hint?.nodeHash)
144
+ if (!isHex64(nodeHash)) return
145
+ const net = loadNetwork()
146
+ const now = Date.now()
147
+ const ttlMs = Number.isFinite(hint.ttlMs) ? hint.ttlMs : DEFAULT_EXPLORE_TTL_MS
148
+ const expiresAt = Number.isFinite(hint.expiresAt) ? hint.expiresAt : now + ttlMs
149
+ const source = String(hint.source || 'unknown')
150
+ const priorSources = new Set(net.hints.filter(h => h.nodeHash === nodeHash).map(h => String(h.source || 'unknown')))
151
+ priorSources.add(source)
152
+ const multiSourceBoost = priorSources.size >= 2 ? 1.2 : 1
153
+ const baseWeight = Number.isFinite(hint.weight) ? hint.weight : 0.1
154
+ if (!net.explorePeers.includes(nodeHash))
155
+ net.explorePeers.push(nodeHash)
156
+ net.hints = net.hints.filter(h => h.nodeHash !== nodeHash || h.kind !== hint.kind)
157
+ net.hints.push({
158
+ nodeHash,
159
+ source,
160
+ kind: String(hint.kind || 'hint'),
161
+ weight: baseWeight * multiSourceBoost,
162
+ expiresAt,
163
+ ...hint.groupId ? { groupId: String(hint.groupId).trim() } : {},
164
+ })
165
+ saveNetwork(net)
166
+ }
167
+
168
+ /**
169
+ * @param {{ remoteNodeHash?: string }[]} roster Trystero roster
170
+ * @param {string} [groupId] 来源群
171
+ * @param {string} [source='roster'] hint 来源标签
172
+ * @returns {void}
173
+ */
174
+ export function recordExplorePeersFromRoster(roster, groupId = '', source = 'roster') {
175
+ if (!roster?.length) return
176
+ const net = loadNetwork()
177
+ const now = Date.now()
178
+ for (const peer of roster) {
179
+ const nodeHash = normalizeHex64(peer?.remoteNodeHash)
180
+ if (!isHex64(nodeHash)) continue
181
+ if (!net.explorePeers.includes(nodeHash))
182
+ net.explorePeers.push(nodeHash)
183
+ net.hints.push({
184
+ nodeHash,
185
+ source: String(source || 'roster'),
186
+ kind: 'roster',
187
+ weight: 0.1,
188
+ expiresAt: now + 24 * 60 * 60 * 1000,
189
+ ...groupId ? { groupId: String(groupId).trim() } : {},
190
+ })
191
+ }
192
+ net.hints = capHintsBySource(net.hints).slice(-MAX_HINTS)
193
+ net.lastRosterAt = now
194
+ saveNetwork(net)
195
+ }
196
+
197
+ /**
198
+ * 疑似分区/eclipse 后:用 trusted 锚点加宽 explore,便于恢复联邦可达。
199
+ * @returns {void}
200
+ */
201
+ export function widenExploreFromTrustedAnchors() {
202
+ if (!isNodeInitialized()) return
203
+ const net = loadNetwork()
204
+ const now = Date.now()
205
+ for (const raw of net.trustedPeers.slice(0, 12)) {
206
+ const nodeHash = normalizeHex64(raw)
207
+ if (!isHex64(nodeHash)) continue
208
+ if (!net.explorePeers.includes(nodeHash))
209
+ net.explorePeers.push(nodeHash)
210
+ net.hints.push({
211
+ nodeHash,
212
+ source: 'recovery:trusted',
213
+ kind: 'partition_recovery',
214
+ weight: 0.35,
215
+ expiresAt: now + 6 * 60 * 60 * 1000,
216
+ })
217
+ }
218
+ net.hints = capHintsBySource(net.hints).slice(-MAX_HINTS)
219
+ net.explorePeers = net.explorePeers.slice(-MAX_EXPLORE)
220
+ saveNetwork(net)
221
+ }
222
+
223
+ /**
224
+ * 增量合并 trusted/explore 池(不覆盖已有全局池)。
225
+ * @param {{ trustedPeers?: string[], explorePeers?: string[] }} patch 增量
226
+ * @returns {void}
227
+ */
228
+ export function mergeNetworkPeerPools(patch = {}) {
229
+ const net = loadNetwork()
230
+ for (const raw of patch.trustedPeers || []) {
231
+ const id = normalizeHex64(raw)
232
+ if (isHex64(id) && !net.trustedPeers.includes(id)) net.trustedPeers.push(id)
233
+ }
234
+ for (const raw of patch.explorePeers || []) {
235
+ const id = normalizeHex64(raw)
236
+ if (isHex64(id) && !net.explorePeers.includes(id)) net.explorePeers.push(id)
237
+ }
238
+ net.lastRosterAt = Date.now()
239
+ saveNetwork(net)
240
+ }
241
+
242
+ /**
243
+ * @param {string[]} nodeHashes trusted 候选
244
+ * @returns {void}
245
+ */
246
+ export function mergeTrustedPeers(nodeHashes) {
247
+ mergeNetworkPeerPools({ trustedPeers: nodeHashes })
248
+ }
249
+
250
+ /**
251
+ * @param {string} groupId 群 scope
252
+ * @param {'node' | 'subject' | 'entity'} scope denylist 作用域
253
+ * @returns {string[]} 规范化 value 列表
254
+ */
255
+ function denyValuesForScope(groupId, scope) {
256
+ const gid = String(groupId || '').trim()
257
+ return [...new Set(
258
+ loadDenylist().blocked
259
+ .filter(entry => entry.scope === scope)
260
+ .filter(entry => scope === 'entity' || !entry.groupId || !gid || entry.groupId === gid)
261
+ .map(entry => entry.value),
262
+ )]
263
+ }
264
+
265
+ /**
266
+ * 节点级 network + 群 scope denylist 视图(供 peer_pool 选取)。
267
+ * deniedNodes 用于连接池过滤;deniedSubjects/deniedEntities 供入站校验。
268
+ * @param {string} [groupId] 群 scope;空则仅全局 deny
269
+ * @returns {PeerPoolView} 连接池视图
270
+ */
271
+ export function loadPeerPoolView(groupId = '') {
272
+ const net = loadNetwork()
273
+ const deniedNodes = denyValuesForScope(groupId, 'node')
274
+ const deniedSubjects = denyValuesForScope(groupId, 'subject')
275
+ const deniedEntities = denyValuesForScope(groupId, 'entity')
276
+ /** @type {Map<string, string>} */
277
+ const hintSources = new Map()
278
+ for (const hint of net.hints)
279
+ if (!hintSources.has(hint.nodeHash))
280
+ hintSources.set(hint.nodeHash, hint.source)
281
+
282
+ return {
283
+ trustedPeers: net.trustedPeers,
284
+ explorePeers: net.explorePeers,
285
+ blockedPeers: deniedNodes,
286
+ deniedNodes,
287
+ deniedSubjects,
288
+ deniedEntities,
289
+ lastRosterAt: net.lastRosterAt,
290
+ hintSources,
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @param {PeerPoolView} view 连接池视图
296
+ * @param {string} key nodeHash / pubKeyHash / entityHash 键
297
+ * @returns {boolean} 是否命中 denylist(按 scope 匹配)
298
+ */
299
+ export function isPeerPoolKeyBlocked(view, key) {
300
+ const normalized = String(key || '').trim().toLowerCase()
301
+ if (!normalized) return false
302
+ if (view.deniedNodes.includes(normalized)) return true
303
+ if (view.deniedSubjects.includes(normalized)) return true
304
+ if (isEntityHash128(normalized) && view.deniedEntities.includes(normalized)) return true
305
+ return false
306
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * 按实体的个人列表(Chat 与 Social 共享)。
3
+ * **block**:对外联邦公开拉黑(timeline block 事件 + personal_block 索引);
4
+ * **hide**:纯本地隐藏(personal_hide.json,永不联邦)。
5
+ */
6
+ import { parseEntityHash } from '../core/entity_id.mjs'
7
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
8
+ import { isWritableLocalEntity } from '../entity/replica.mjs'
9
+
10
+ import { isNodeInitialized, getEntityStore } from './instance.mjs'
11
+
12
+ /** @typedef {'subject' | 'entity'} PersonalListScope */
13
+
14
+ const HIDE_JSON = 'personal_hide.json'
15
+ const MUTE_JSON = 'personal_mute.json'
16
+ const BLOCK_INDEX_JSON = 'personal_block.json'
17
+
18
+ /**
19
+ * @param {string} targetEntityHash 128 位十六进制
20
+ * @returns {Array<{ scope: PersonalListScope, value: string }>} 规范化条目
21
+ */
22
+ export function entriesForTargetEntityHash(targetEntityHash) {
23
+ const parsed = parseEntityHash(targetEntityHash)
24
+ if (!parsed) return []
25
+ /** @type {Map<string, { scope: PersonalListScope, value: string }>} */
26
+ const byKey = new Map()
27
+ byKey.set(`entity:${parsed.entityHash}`, { scope: 'entity', value: parsed.entityHash })
28
+ if (isHex64(parsed.subjectHash))
29
+ byKey.set(`subject:${parsed.subjectHash}`, { scope: 'subject', value: parsed.subjectHash })
30
+ return [...byKey.values()]
31
+ }
32
+
33
+ /**
34
+ * @param {Array<{ scope?: string, value?: string }>} raw 原始条目
35
+ * @returns {Array<{ scope: PersonalListScope, value: string }>} 去重规范化
36
+ */
37
+ export function normalizePersonalListEntries(raw) {
38
+ /** @type {Map<string, { scope: PersonalListScope, value: string }>} */
39
+ const byKey = new Map()
40
+ for (const entry of raw || []) {
41
+ const scope = String(entry?.scope || '').trim().toLowerCase()
42
+ const value = String(entry?.value || '').trim().toLowerCase()
43
+ if (scope === 'entity' && parseEntityHash(value))
44
+ byKey.set(`entity:${value}`, { scope: 'entity', value })
45
+ else if (scope === 'subject' && isHex64(normalizeHex64(value)))
46
+ byKey.set(`subject:${normalizeHex64(value)}`, { scope: 'subject', value: normalizeHex64(value) })
47
+ }
48
+ return [...byKey.values()]
49
+ }
50
+
51
+ /**
52
+ * @param {Array<{ scope: PersonalListScope, value: string }>} entries 列表
53
+ * @param {object} subject 待检主体
54
+ * @param {string} [subject.entityHash] 作者实体
55
+ * @param {string} [subject.pubKeyHash] 用户公钥哈希
56
+ * @param {string} [subject.subjectHash] 主体哈希
57
+ * @param {string} [subject.nodeHash] 节点哈希
58
+ * @returns {boolean} 是否命中列表
59
+ */
60
+ export function matchesPersonalListEntries(entries, subject) {
61
+ const entity = String(subject?.entityHash || '').trim().toLowerCase()
62
+ const pk = normalizeHex64(subject?.pubKeyHash || subject?.subjectHash || '')
63
+ if (entity) {
64
+ for (const entry of entries)
65
+ if (entry.scope === 'entity' && entry.value === entity) return true
66
+ const parsed = parseEntityHash(entity)
67
+ if (parsed)
68
+ for (const entry of entries)
69
+ if (entry.scope === 'subject' && entry.value === parsed.subjectHash) return true
70
+ }
71
+ if (isHex64(pk))
72
+ for (const entry of entries)
73
+ if (entry.scope === 'subject' && entry.value === pk) return true
74
+ return false
75
+ }
76
+
77
+ /**
78
+ * @param {string} viewerEntityHash 观看者实体
79
+ * @returns {Promise<Array<{ scope: PersonalListScope, value: string }>>} 隐藏条目
80
+ */
81
+ export async function loadPersonalHideEntries(viewerEntityHash) {
82
+ if (!isNodeInitialized()) return []
83
+ const data = await getEntityStore().readEntityJson(viewerEntityHash, HIDE_JSON)
84
+ return normalizePersonalListEntries(data?.hidden || [])
85
+ }
86
+
87
+ /**
88
+ * @param {string} viewerEntityHash 观看者实体
89
+ * @returns {Promise<Array<{ scope: PersonalListScope, value: string }>>} 静音条目
90
+ */
91
+ export async function loadPersonalMuteEntries(viewerEntityHash) {
92
+ if (!isNodeInitialized()) return []
93
+ const data = await getEntityStore().readEntityJson(viewerEntityHash, MUTE_JSON)
94
+ return normalizePersonalListEntries(data?.muted || [])
95
+ }
96
+
97
+ /**
98
+ * @param {string} viewerEntityHash 观看者实体
99
+ * @returns {Promise<Array<{ scope: PersonalListScope, value: string }>>} 拉黑条目
100
+ */
101
+ export async function loadPersonalBlockEntries(viewerEntityHash) {
102
+ if (!isNodeInitialized()) return []
103
+ const data = await getEntityStore().readEntityJson(viewerEntityHash, BLOCK_INDEX_JSON)
104
+ return normalizePersonalListEntries(data?.blocked || [])
105
+ }
106
+
107
+ /**
108
+ * @param {string} viewerEntityHash 观看者实体
109
+ * @param {object} subject 待检主体
110
+ * @returns {Promise<boolean>} 是否被隐藏
111
+ */
112
+ export async function isHiddenBy(viewerEntityHash, subject) {
113
+ return matchesPersonalListEntries(await loadPersonalHideEntries(viewerEntityHash), subject)
114
+ }
115
+
116
+ /**
117
+ * @param {string} viewerEntityHash 观看者实体
118
+ * @param {object} subject 待检主体
119
+ * @returns {Promise<boolean>} 是否被拉黑
120
+ */
121
+ export async function isBlockedBy(viewerEntityHash, subject) {
122
+ return matchesPersonalListEntries(await loadPersonalBlockEntries(viewerEntityHash), subject)
123
+ }
124
+
125
+ /**
126
+ * @param {string} viewerEntityHash 观看者实体
127
+ * @param {object} subject 待检主体
128
+ * @returns {Promise<boolean>} 是否应过滤
129
+ */
130
+ export async function isFilteredByPersonalLists(viewerEntityHash, subject) {
131
+ if (!viewerEntityHash) return false
132
+ const [blocked, hidden, muted] = await Promise.all([
133
+ loadPersonalBlockEntries(viewerEntityHash),
134
+ loadPersonalHideEntries(viewerEntityHash),
135
+ loadPersonalMuteEntries(viewerEntityHash),
136
+ ])
137
+ return matchesPersonalListEntries(blocked, subject)
138
+ || matchesPersonalListEntries(hidden, subject)
139
+ || matchesPersonalListEntries(muted, subject)
140
+ }
141
+
142
+ /**
143
+ * @param {string} viewerEntityHash 本地可写实体
144
+ * @param {string} targetEntityHash 目标
145
+ * @param {boolean} hide true=隐藏
146
+ * @returns {Promise<boolean>} 当前是否隐藏
147
+ */
148
+ export async function setPersonalHidden(viewerEntityHash, targetEntityHash, hide) {
149
+ if (!isWritableLocalEntity(viewerEntityHash)) throw new Error('entity not writable on this replica')
150
+ const target = String(targetEntityHash || '').trim().toLowerCase()
151
+ if (!parseEntityHash(target)) throw new Error('invalid targetEntityHash')
152
+ const store = getEntityStore()
153
+ const current = normalizePersonalListEntries((await store.readEntityJson(viewerEntityHash, HIDE_JSON))?.hidden || [])
154
+ const addEntries = entriesForTargetEntityHash(target)
155
+ const addKeys = new Set(addEntries.map(e => `${e.scope}:${e.value}`))
156
+ const next = hide
157
+ ? normalizePersonalListEntries([...current, ...addEntries])
158
+ : current.filter(entry => !addKeys.has(`${entry.scope}:${entry.value}`))
159
+ await store.writeEntityJson(viewerEntityHash, HIDE_JSON, { hidden: next })
160
+ return hide
161
+ }
162
+
163
+ /**
164
+ * @param {string} viewerEntityHash 本地可写实体
165
+ * @param {string} targetEntityHash 目标
166
+ * @param {boolean} mute true=静音
167
+ * @returns {Promise<boolean>} 当前是否静音
168
+ */
169
+ export async function setPersonalMuted(viewerEntityHash, targetEntityHash, mute) {
170
+ if (!isWritableLocalEntity(viewerEntityHash)) throw new Error('entity not writable on this replica')
171
+ const target = String(targetEntityHash || '').trim().toLowerCase()
172
+ if (!parseEntityHash(target)) throw new Error('invalid targetEntityHash')
173
+ const store = getEntityStore()
174
+ const current = normalizePersonalListEntries((await store.readEntityJson(viewerEntityHash, MUTE_JSON))?.muted || [])
175
+ const addEntries = entriesForTargetEntityHash(target)
176
+ const addKeys = new Set(addEntries.map(e => `${e.scope}:${e.value}`))
177
+ const next = mute
178
+ ? normalizePersonalListEntries([...current, ...addEntries])
179
+ : current.filter(entry => !addKeys.has(`${entry.scope}:${entry.value}`))
180
+ await store.writeEntityJson(viewerEntityHash, MUTE_JSON, { muted: next })
181
+ return mute
182
+ }
183
+
184
+ /**
185
+ * @param {string} viewerEntityHash 观看者实体
186
+ * @param {object} subject 待检主体
187
+ * @returns {Promise<boolean>} 是否被静音
188
+ */
189
+ export async function isMutedBy(viewerEntityHash, subject) {
190
+ return matchesPersonalListEntries(await loadPersonalMuteEntries(viewerEntityHash), subject)
191
+ }
192
+
193
+ /**
194
+ * 从物化公开拉黑名单同步本地索引(真相源 = 时间线 blocked 集)。
195
+ * @param {string} viewerEntityHash 实体
196
+ * @param {string[]} blockedEntityHashes 物化 blocked entityHash 列表
197
+ * @returns {Promise<void>}
198
+ */
199
+ export async function rebuildPersonalBlockIndex(viewerEntityHash, blockedEntityHashes) {
200
+ if (!isWritableLocalEntity(viewerEntityHash)) return
201
+ /** @type {Map<string, { scope: PersonalListScope, value: string }>} */
202
+ const byKey = new Map()
203
+ for (const raw of blockedEntityHashes || [])
204
+ for (const entry of entriesForTargetEntityHash(raw))
205
+ byKey.set(`${entry.scope}:${entry.value}`, entry)
206
+
207
+ await getEntityStore().writeEntityJson(viewerEntityHash, BLOCK_INDEX_JSON, {
208
+ blocked: [...byKey.values()],
209
+ })
210
+ }
211
+
212
+ /**
213
+ * 将 personal-lists API `{ entries }` 转为内存过滤集。
214
+ * @param {Array<{ scope?: string, kind?: string, value?: string }>} entries API 条目
215
+ * @returns {{ blockedEntityHashes: Set<string>, blockedSubjects: Set<string>, hiddenEntityHashes: Set<string>, hiddenSubjects: Set<string> }} 过滤集
216
+ */
217
+ export function filterSetsFromPersonalListEntries(entries) {
218
+ /** @type {Set<string>} */
219
+ const blockedEntityHashes = new Set()
220
+ /** @type {Set<string>} */
221
+ const blockedSubjects = new Set()
222
+ /** @type {Set<string>} */
223
+ const hiddenEntityHashes = new Set()
224
+ /** @type {Set<string>} */
225
+ const hiddenSubjects = new Set()
226
+ /** @type {Set<string>} */
227
+ const mutedEntityHashes = new Set()
228
+ /** @type {Set<string>} */
229
+ const mutedSubjects = new Set()
230
+ for (const entry of entries || []) {
231
+ const kind = String(entry?.kind || '').trim().toLowerCase()
232
+ const scope = String(entry?.scope || '').trim().toLowerCase()
233
+ const value = String(entry?.value || '').trim().toLowerCase()
234
+ if (!value || (scope !== 'entity' && scope !== 'subject')) continue
235
+ if (kind === 'block')
236
+ if (scope === 'entity') blockedEntityHashes.add(value)
237
+ else blockedSubjects.add(value)
238
+
239
+ else if (kind === 'hide')
240
+ if (scope === 'entity') hiddenEntityHashes.add(value)
241
+ else hiddenSubjects.add(value)
242
+ else if (kind === 'mute')
243
+ if (scope === 'entity') mutedEntityHashes.add(value)
244
+ else mutedSubjects.add(value)
245
+
246
+ }
247
+ return { blockedEntityHashes, blockedSubjects, hiddenEntityHashes, hiddenSubjects, mutedEntityHashes, mutedSubjects }
248
+ }
249
+
250
+ /**
251
+ * @param {string} viewerEntityHash 观看者实体
252
+ * @returns {Promise<{ blockedEntityHashes: Set<string>, blockedSubjects: Set<string>, hiddenEntityHashes: Set<string>, hiddenSubjects: Set<string> }>} 过滤集
253
+ */
254
+ export async function loadPersonalFilterSets(viewerEntityHash) {
255
+ if (!viewerEntityHash)
256
+ return filterSetsFromPersonalListEntries([])
257
+ const [blockedEntries, hiddenEntries, mutedEntries] = await Promise.all([
258
+ loadPersonalBlockEntries(viewerEntityHash),
259
+ loadPersonalHideEntries(viewerEntityHash),
260
+ loadPersonalMuteEntries(viewerEntityHash),
261
+ ])
262
+ return filterSetsFromPersonalListEntries([
263
+ ...blockedEntries.map(entry => ({ ...entry, kind: 'block' })),
264
+ ...hiddenEntries.map(entry => ({ ...entry, kind: 'hide' })),
265
+ ...mutedEntries.map(entry => ({ ...entry, kind: 'mute' })),
266
+ ])
267
+ }
268
+
269
+ /**
270
+ * @param {object} filterSets loadPersonalFilterSets 结果
271
+ * @param {string} authorEntityHash 作者实体
272
+ * @returns {boolean} 是否应过滤
273
+ */
274
+ export function isAuthorFilteredByPersonalSets(filterSets, authorEntityHash) {
275
+ const entity = String(authorEntityHash || '').trim().toLowerCase()
276
+ if (!entity) return false
277
+ if (filterSets.blockedEntityHashes.has(entity) || filterSets.hiddenEntityHashes.has(entity) || filterSets.mutedEntityHashes?.has(entity))
278
+ return true
279
+ const parsed = parseEntityHash(entity)
280
+ if (!parsed) return false
281
+ if (filterSets.blockedSubjects.has(parsed.subjectHash)
282
+ || filterSets.hiddenSubjects.has(parsed.subjectHash)
283
+ || filterSets.mutedSubjects?.has(parsed.subjectHash))
284
+ return true
285
+ return false
286
+ }