@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.
- package/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { compositeKey } from '../core/composite_key.mjs'
|
|
2
|
+
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
3
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
|
+
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
5
|
+
|
|
6
|
+
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
7
|
+
|
|
8
|
+
const DATA_NAME = 'denylist'
|
|
9
|
+
|
|
10
|
+
/** @typedef {'subject' | 'entity' | 'node'} DenyScope — 节点连接拒绝(deny),非 Social block / 群 ban */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* blocked: Array<{ scope: DenyScope, value: string, groupId?: string }>
|
|
15
|
+
* keys: Set<string>
|
|
16
|
+
* }} DenylistIndex
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {DenylistIndex | null} */
|
|
20
|
+
let cachedIndex = null
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 串行化 denylist 写路径,避免并发 load/save 覆写。
|
|
24
|
+
* @param {() => void | Promise<void>} mutator 突变
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
function mutateDenylist(mutator) {
|
|
28
|
+
return withAsyncMutex('denylist', mutator)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {DenyScope} scope 拉黑范围
|
|
33
|
+
* @param {string} groupId 群 ID 或 `*`
|
|
34
|
+
* @param {string} value 键值
|
|
35
|
+
* @returns {string} 索引键
|
|
36
|
+
*/
|
|
37
|
+
function denyKey(scope, groupId, value) {
|
|
38
|
+
return compositeKey(scope, groupId, value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {Array<{ scope: DenyScope, value: string, groupId?: string }>} blocked 条目
|
|
43
|
+
* @returns {DenylistIndex} 内存索引
|
|
44
|
+
*/
|
|
45
|
+
function buildDenylistIndex(blocked) {
|
|
46
|
+
/** @type {DenylistIndex} */
|
|
47
|
+
const index = { blocked, keys: new Set() }
|
|
48
|
+
for (const entry of blocked) {
|
|
49
|
+
const gid = String(entry.groupId || '').trim() || '*'
|
|
50
|
+
if (entry.scope === 'entity') {
|
|
51
|
+
index.keys.add(denyKey('entity', '*', entry.value))
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
index.keys.add(denyKey(entry.scope, gid, entry.value))
|
|
55
|
+
}
|
|
56
|
+
return index
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
export function invalidateDenylistIndex() {
|
|
63
|
+
cachedIndex = null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @returns {DenylistIndex} 缓存索引
|
|
68
|
+
*/
|
|
69
|
+
function getDenylistIndex() {
|
|
70
|
+
if (cachedIndex) return cachedIndex
|
|
71
|
+
const raw = readNodeJsonSync(DATA_NAME)
|
|
72
|
+
const blocked = normalizeDenylist(raw).blocked
|
|
73
|
+
cachedIndex = buildDenylistIndex(blocked)
|
|
74
|
+
return cachedIndex
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} raw 磁盘 JSON 或请求体
|
|
79
|
+
* @returns {{ blocked: Array<{ scope: DenyScope, value: string, groupId?: string }> }} 规范化 denylist
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeDenylist(raw) {
|
|
82
|
+
/** @type {Array<{ scope: DenyScope, value: string, groupId?: string }>} */
|
|
83
|
+
const blocked = []
|
|
84
|
+
for (const entry of raw?.blocked || []) {
|
|
85
|
+
const scope = String(entry?.scope || '').trim().toLowerCase()
|
|
86
|
+
const value = String(entry?.value || '').trim().toLowerCase()
|
|
87
|
+
const groupId = String(entry.groupId || '').trim()
|
|
88
|
+
if (!scope || !value) continue
|
|
89
|
+
if (scope === 'entity') {
|
|
90
|
+
if (isEntityHash128(value))
|
|
91
|
+
blocked.push({ scope: 'entity', value })
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (scope === 'node' && isHex64(normalizeHex64(value)))
|
|
95
|
+
blocked.push({ scope: 'node', value: normalizeHex64(value), ...groupId ? { groupId } : {} })
|
|
96
|
+
else if (scope === 'subject' && isHex64(normalizeHex64(value)))
|
|
97
|
+
blocked.push({ scope: 'subject', value: normalizeHex64(value), ...groupId ? { groupId } : {} })
|
|
98
|
+
}
|
|
99
|
+
return { blocked }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @returns {{ blocked: Array<{ scope: DenyScope, value: string, groupId?: string }> }} 节点级 denylist
|
|
104
|
+
*/
|
|
105
|
+
export function loadDenylist() {
|
|
106
|
+
return { blocked: getDenylistIndex().blocked }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {{ blocked: Array<{ scope: DenyScope, value: string, groupId?: string }> }} list denylist
|
|
111
|
+
* @returns {void}
|
|
112
|
+
*/
|
|
113
|
+
export function saveDenylist(list) {
|
|
114
|
+
const blocked = normalizeDenylist(list).blocked
|
|
115
|
+
writeNodeJsonSync(DATA_NAME, { blocked })
|
|
116
|
+
cachedIndex = buildDenylistIndex(blocked)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {object} state 物化群状态
|
|
121
|
+
* @param {object} subject 待检主体
|
|
122
|
+
* @returns {boolean} 是否命中群级 ban 集合
|
|
123
|
+
*/
|
|
124
|
+
export function isSubjectBannedByState(state, subject) {
|
|
125
|
+
const pk = normalizeHex64(subject?.pubKeyHash)
|
|
126
|
+
if (isHex64(pk) && state?.bannedMembers?.has?.(pk)) return true
|
|
127
|
+
const entity = String(subject?.entityHash || '').trim().toLowerCase()
|
|
128
|
+
if (isEntityHash128(entity) && state?.bannedEntities?.has?.(entity)) return true
|
|
129
|
+
const node = normalizeHex64(subject?.nodeHash)
|
|
130
|
+
if (isHex64(node) && state?.bannedNodes?.has?.(node)) return true
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {DenylistIndex} index 内存索引
|
|
136
|
+
* @param {object} subject 待检主体
|
|
137
|
+
* @param {string} [groupId] 可选群 scope
|
|
138
|
+
* @returns {boolean} 是否命中
|
|
139
|
+
*/
|
|
140
|
+
function matchesDenylistIndex(index, subject, groupId = '') {
|
|
141
|
+
const pk = normalizeHex64(subject?.pubKeyHash)
|
|
142
|
+
const entity = String(subject?.entityHash || '').trim().toLowerCase()
|
|
143
|
+
const node = normalizeHex64(subject?.nodeHash)
|
|
144
|
+
const gid = String(groupId || '').trim()
|
|
145
|
+
const { keys } = index
|
|
146
|
+
|
|
147
|
+
if (entity && keys.has(denyKey('entity', '*', entity))) return true
|
|
148
|
+
if (isHex64(node) && keys.has(denyKey('node', '*', node))) return true
|
|
149
|
+
if (isHex64(pk) && keys.has(denyKey('subject', '*', pk))) return true
|
|
150
|
+
if (!gid) return false
|
|
151
|
+
if (isHex64(pk) && keys.has(denyKey('subject', gid, pk))) return true
|
|
152
|
+
if (isHex64(node) && keys.has(denyKey('node', gid, node))) return true
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {object} subject 待检主体
|
|
158
|
+
* @param {string} [groupId] 可选群 scope
|
|
159
|
+
* @returns {boolean} 是否在节点级 denylist 中(deny,非 Social block)
|
|
160
|
+
*/
|
|
161
|
+
export function isSubjectBlocked(subject, groupId = '') {
|
|
162
|
+
return matchesDenylistIndex(getDenylistIndex(), subject, groupId)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} groupId 群 ID
|
|
167
|
+
* @param {string} peerKey pubKeyHash 或 nodeHash(按 scope 分别匹配,不混填)
|
|
168
|
+
* @returns {boolean} 是否拉黑
|
|
169
|
+
*/
|
|
170
|
+
export function isPeerKeyBlocked(groupId, peerKey) {
|
|
171
|
+
const key = normalizeHex64(peerKey)
|
|
172
|
+
if (!isHex64(key)) return false
|
|
173
|
+
const index = getDenylistIndex()
|
|
174
|
+
const gid = String(groupId || '').trim()
|
|
175
|
+
const { keys } = index
|
|
176
|
+
if (keys.has(denyKey('subject', '*', key))) return true
|
|
177
|
+
if (keys.has(denyKey('node', '*', key))) return true
|
|
178
|
+
if (!gid) return false
|
|
179
|
+
if (keys.has(denyKey('subject', gid, key))) return true
|
|
180
|
+
if (keys.has(denyKey('node', gid, key))) return true
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {string} pubKeyHash 64 位十六进制
|
|
186
|
+
* @returns {boolean} 是否拉黑该 subject
|
|
187
|
+
*/
|
|
188
|
+
export function isPubKeyHashBlocked(pubKeyHash) {
|
|
189
|
+
return isSubjectBlocked({ pubKeyHash })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {string} entityHash 128 位十六进制
|
|
194
|
+
* @returns {boolean} 是否拉黑该 entity
|
|
195
|
+
*/
|
|
196
|
+
export function isEntityHashBlocked(entityHash) {
|
|
197
|
+
return isSubjectBlocked({ entityHash })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 追加拉黑并落盘。
|
|
202
|
+
* @param {{ scope: DenyScope, value: string, groupId?: string }} entry 拉黑项
|
|
203
|
+
* @returns {Promise<void>}
|
|
204
|
+
*/
|
|
205
|
+
export function addDenylistEntry(entry) {
|
|
206
|
+
const scope = String(entry?.scope || '').trim().toLowerCase()
|
|
207
|
+
const value = String(entry?.value || '').trim().toLowerCase()
|
|
208
|
+
if (!scope || !value)
|
|
209
|
+
throw new Error('scope and value required')
|
|
210
|
+
if (scope === 'entity' && entry.groupId)
|
|
211
|
+
throw new Error('entity scope does not use groupId')
|
|
212
|
+
if (scope === 'subject' && !isHex64(normalizeHex64(value)))
|
|
213
|
+
throw new Error('invalid pubKeyHash')
|
|
214
|
+
if (scope === 'entity' && !isEntityHash128(value))
|
|
215
|
+
throw new Error('invalid entityHash')
|
|
216
|
+
if (scope === 'node' && !isHex64(normalizeHex64(value)))
|
|
217
|
+
throw new Error('invalid nodeHash')
|
|
218
|
+
|
|
219
|
+
const normValue = scope === 'node' || scope === 'subject' ? normalizeHex64(value) : value
|
|
220
|
+
const groupId = entry.groupId ? String(entry.groupId).trim() : undefined
|
|
221
|
+
return mutateDenylist(() => {
|
|
222
|
+
const list = loadDenylist()
|
|
223
|
+
if (list.blocked.some(row => row.scope === scope && row.value === normValue && row.groupId === groupId))
|
|
224
|
+
return
|
|
225
|
+
list.blocked.push(groupId ? { scope, value: normValue, groupId } : { scope, value: normValue })
|
|
226
|
+
saveDenylist(list)
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {object} banContent member_ban 内容
|
|
232
|
+
* @param {string} [groupId] 来源群
|
|
233
|
+
* @returns {Promise<void>}
|
|
234
|
+
*/
|
|
235
|
+
export async function addDenylistFromBanContent(banContent, groupId) {
|
|
236
|
+
const scope = String(banContent?.banScope || 'entity').trim().toLowerCase()
|
|
237
|
+
const sourceGroupId = String(groupId || '').trim()
|
|
238
|
+
if (scope === 'entity' && banContent?.targetEntityHash)
|
|
239
|
+
await addDenylistEntry({ scope: 'entity', value: banContent.targetEntityHash })
|
|
240
|
+
if (scope === 'node' && banContent?.targetNodeHash)
|
|
241
|
+
await addDenylistEntry({ scope: 'node', value: banContent.targetNodeHash })
|
|
242
|
+
const pk = normalizeHex64(banContent?.targetPubKeyHash)
|
|
243
|
+
if (isHex64(pk))
|
|
244
|
+
await addDenylistEntry({ scope: 'subject', value: pk, ...sourceGroupId ? { groupId: sourceGroupId } : {} })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {string} entityHash 128 位十六进制
|
|
249
|
+
* @param {boolean} block true=拉黑
|
|
250
|
+
* @returns {Promise<boolean>} 当前是否拉黑
|
|
251
|
+
*/
|
|
252
|
+
export async function setEntityBlocked(entityHash, block) {
|
|
253
|
+
const id = String(entityHash || '').trim().toLowerCase()
|
|
254
|
+
if (!isEntityHash128(id)) throw new Error('invalid entityHash')
|
|
255
|
+
await mutateDenylist(() => {
|
|
256
|
+
const list = loadDenylist()
|
|
257
|
+
const without = list.blocked.filter(e => !(e.scope === 'entity' && e.value === id))
|
|
258
|
+
if (block) without.push({ scope: 'entity', value: id })
|
|
259
|
+
saveDenylist({ blocked: without })
|
|
260
|
+
})
|
|
261
|
+
return block
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 追加群 scope 拉黑项。
|
|
266
|
+
* @param {string} groupId 群 ID
|
|
267
|
+
* @param {DenyScope} scope subject | entity | node 作用域
|
|
268
|
+
* @param {string} value 键值
|
|
269
|
+
* @returns {Promise<void>}
|
|
270
|
+
*/
|
|
271
|
+
export function addGroupBlockedPeer(groupId, scope, value) {
|
|
272
|
+
const normScope = String(scope || '').trim().toLowerCase()
|
|
273
|
+
if (normScope === 'entity')
|
|
274
|
+
return addDenylistEntry({ scope: normScope, value })
|
|
275
|
+
return addDenylistEntry({ scope: normScope, value, groupId })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {string} groupId 群 ID
|
|
280
|
+
* @param {DenyScope} scope subject | entity | node 作用域
|
|
281
|
+
* @param {string} value 键值
|
|
282
|
+
* @returns {Promise<void>}
|
|
283
|
+
*/
|
|
284
|
+
export function removeGroupBlockedPeer(groupId, scope, value) {
|
|
285
|
+
const normScope = String(scope || '').trim().toLowerCase()
|
|
286
|
+
const id = String(value || '').trim().toLowerCase()
|
|
287
|
+
if (!normScope || !id) return Promise.resolve()
|
|
288
|
+
return mutateDenylist(() => {
|
|
289
|
+
const list = loadDenylist()
|
|
290
|
+
list.blocked = list.blocked.filter(entry => {
|
|
291
|
+
if (entry.scope !== normScope || entry.value !== id) return true
|
|
292
|
+
if (normScope === 'entity') return false
|
|
293
|
+
return entry.groupId !== groupId
|
|
294
|
+
})
|
|
295
|
+
saveDenylist(list)
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} groupId 群 ID
|
|
301
|
+
* @param {Array<{ scope: DenyScope, value: string }>} entries 拉黑条目
|
|
302
|
+
* @returns {Promise<void>}
|
|
303
|
+
*/
|
|
304
|
+
export async function addGroupBlockedPeers(groupId, entries) {
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (!entry?.scope || !entry?.value) continue
|
|
307
|
+
await addGroupBlockedPeer(groupId, entry.scope, entry.value)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import fsp from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
6
|
+
import { assertSafeEvfsLogicalPath } from '../core/evfs_logical_path.mjs'
|
|
7
|
+
import { readJsonFile, writeJsonFile } from '../utils/json_io.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} EntityStore
|
|
11
|
+
* @property {(entityHash: string) => Promise<string[]>} listEntityFiles 列出逻辑路径
|
|
12
|
+
* @property {(entityHash: string, logicalPath: string) => Promise<Buffer | null>} readEntityFile
|
|
13
|
+
* @property {(entityHash: string, logicalPath: string, data: Buffer | Uint8Array) => Promise<void>} writeEntityFile
|
|
14
|
+
* @property {(entityHash: string, logicalPath: string) => Promise<boolean>} statEntityFile
|
|
15
|
+
* @property {(entityHash: string, name: string) => Promise<object | null>} readEntityJson
|
|
16
|
+
* @property {(entityHash: string, name: string, data: object) => Promise<void>} writeEntityJson
|
|
17
|
+
* @property {(entityHash: string, logicalPath: string) => Promise<object | null>} readManifest
|
|
18
|
+
* @property {(entityHash: string, logicalPath: string, data: object) => Promise<void>} writeManifest
|
|
19
|
+
* @property {(entityHash: string, logicalPath: string) => Promise<boolean>} statManifest
|
|
20
|
+
* @property {() => Promise<string[]>} listEntityHashes
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} entityHash 128 位十六进制
|
|
25
|
+
* @returns {string} 规范化 entityHash
|
|
26
|
+
*/
|
|
27
|
+
function normalizeEntityHash(entityHash) {
|
|
28
|
+
const hash = String(entityHash || '').trim().toLowerCase()
|
|
29
|
+
if (!parseEntityHash(hash)) throw new Error('invalid entityHash')
|
|
30
|
+
return hash
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} baseDir entities 根目录
|
|
35
|
+
* @returns {EntityStore} 默认文件系统 EntityStore
|
|
36
|
+
*/
|
|
37
|
+
export function createFsEntityStore(baseDir) {
|
|
38
|
+
const root = path.resolve(baseDir)
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} entityHash 128 位十六进制
|
|
42
|
+
* @returns {string} 实体目录绝对路径
|
|
43
|
+
*/
|
|
44
|
+
function entityRoot(entityHash) {
|
|
45
|
+
return path.join(root, normalizeEntityHash(entityHash))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} entityHash 128 位十六进制
|
|
50
|
+
* @param {string} name 相对文件名(如 profile.json)
|
|
51
|
+
* @returns {string} JSON 文件绝对路径
|
|
52
|
+
*/
|
|
53
|
+
function entityJsonPath(entityHash, name) {
|
|
54
|
+
const safe = String(name || '').trim().replace(/\\/g, '/')
|
|
55
|
+
if (!safe || safe.includes('..') || safe.startsWith('/')) throw new Error('invalid entity json name')
|
|
56
|
+
return path.join(entityRoot(entityHash), safe)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} entityHash 128 位十六进制
|
|
61
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
62
|
+
* @returns {string} manifest 绝对路径
|
|
63
|
+
*/
|
|
64
|
+
function manifestPath(entityHash, logicalPath) {
|
|
65
|
+
const filesRoot = path.join(entityRoot(entityHash), 'files')
|
|
66
|
+
const safe = assertSafeEvfsLogicalPath(logicalPath)
|
|
67
|
+
const resolved = path.resolve(filesRoot, `${safe}.manifest.json`)
|
|
68
|
+
const rootResolved = path.resolve(filesRoot)
|
|
69
|
+
if (resolved !== rootResolved && !resolved.startsWith(rootResolved + path.sep))
|
|
70
|
+
throw new Error('invalid EVFS path traversal')
|
|
71
|
+
return resolved
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
/**
|
|
76
|
+
* @returns {Promise<string[]>} 本 store 下全部 entityHash
|
|
77
|
+
*/
|
|
78
|
+
async listEntityHashes() {
|
|
79
|
+
try {
|
|
80
|
+
const entries = await fsp.readdir(root, { withFileTypes: true })
|
|
81
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name.toLowerCase()).filter(h => parseEntityHash(h))
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return []
|
|
85
|
+
throw err
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} entityHash 128 位十六进制
|
|
91
|
+
* @param {string} name 相对 JSON 名
|
|
92
|
+
* @returns {Promise<object | null>} 解析后的 JSON 或 null
|
|
93
|
+
*/
|
|
94
|
+
async readEntityJson(entityHash, name) {
|
|
95
|
+
return readJsonFile(entityJsonPath(entityHash, name))
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} entityHash 128 位十六进制
|
|
100
|
+
* @param {string} name 相对 JSON 名
|
|
101
|
+
* @param {object} data 写入对象
|
|
102
|
+
* @returns {Promise<void>}
|
|
103
|
+
*/
|
|
104
|
+
async writeEntityJson(entityHash, name, data) {
|
|
105
|
+
await writeJsonFile(entityJsonPath(entityHash, name), data)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} entityHash 128 位十六进制
|
|
110
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
111
|
+
* @returns {Promise<Buffer | null>} 明文文件或 null
|
|
112
|
+
*/
|
|
113
|
+
async readEntityFile(entityHash, logicalPath) {
|
|
114
|
+
const filePath = manifestPath(entityHash, logicalPath).replace(/\.manifest\.json$/u, '')
|
|
115
|
+
try {
|
|
116
|
+
return await fsp.readFile(filePath)
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (/** @type {NodeJS.ErrnoException} */ err.code === 'ENOENT') return null
|
|
120
|
+
throw err
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} entityHash 128 位十六进制
|
|
126
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
127
|
+
* @param {Buffer | Uint8Array} data 明文内容
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
*/
|
|
130
|
+
async writeEntityFile(entityHash, logicalPath, data) {
|
|
131
|
+
const filePath = manifestPath(entityHash, logicalPath).replace(/\.manifest\.json$/u, '')
|
|
132
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
|
133
|
+
await fsp.writeFile(filePath, Buffer.from(data))
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {string} entityHash 128 位十六进制
|
|
138
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
139
|
+
* @returns {Promise<boolean>} 明文文件是否存在
|
|
140
|
+
*/
|
|
141
|
+
async statEntityFile(entityHash, logicalPath) {
|
|
142
|
+
const filePath = manifestPath(entityHash, logicalPath).replace(/\.manifest\.json$/u, '')
|
|
143
|
+
try {
|
|
144
|
+
await fsp.access(filePath)
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string} entityHash 128 位十六进制
|
|
154
|
+
* @returns {Promise<string[]>} 逻辑路径列表
|
|
155
|
+
*/
|
|
156
|
+
async listEntityFiles(entityHash) {
|
|
157
|
+
const filesRoot = path.join(entityRoot(entityHash), 'files')
|
|
158
|
+
/** @type {string[]} */
|
|
159
|
+
const out = []
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} dir 当前目录
|
|
162
|
+
* @param {string} prefix 相对前缀
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
*/
|
|
165
|
+
async function walk(dir, prefix) {
|
|
166
|
+
let entries
|
|
167
|
+
try { entries = await fsp.readdir(dir, { withFileTypes: true }) }
|
|
168
|
+
catch { return }
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
171
|
+
if (entry.isDirectory()) await walk(path.join(dir, entry.name), rel)
|
|
172
|
+
else if (entry.name.endsWith('.manifest.json'))
|
|
173
|
+
out.push(rel.replace(/\.manifest\.json$/u, ''))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await walk(filesRoot, '')
|
|
177
|
+
return out
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} entityHash 128 位十六进制
|
|
182
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
183
|
+
* @returns {Promise<object | null>} manifest 或 null
|
|
184
|
+
*/
|
|
185
|
+
async readManifest(entityHash, logicalPath) {
|
|
186
|
+
return readJsonFile(manifestPath(entityHash, logicalPath))
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} entityHash 128 位十六进制
|
|
191
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
192
|
+
* @param {object} data manifest 对象
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async writeManifest(entityHash, logicalPath, data) {
|
|
196
|
+
await writeJsonFile(manifestPath(entityHash, logicalPath), data)
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} entityHash 128 位十六进制
|
|
201
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
202
|
+
* @returns {Promise<boolean>} manifest 是否存在
|
|
203
|
+
*/
|
|
204
|
+
async statManifest(entityHash, logicalPath) {
|
|
205
|
+
try {
|
|
206
|
+
await fsp.access(manifestPath(entityHash, logicalPath))
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { userEntityHashFromRecoveryPubKeyHex } from '../core/entity_id.mjs'
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { nodeHashFromSeed } from '../entity/node_hash.mjs'
|
|
6
|
+
import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
|
|
7
|
+
|
|
8
|
+
import { emitNodeChange } from './instance.mjs'
|
|
9
|
+
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
10
|
+
|
|
11
|
+
const NODE_SEED_HEX_RE = /^[0-9a-f]{64}$/iu
|
|
12
|
+
const NODE_JSON = 'node'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {object} 节点配置磁盘对象
|
|
16
|
+
*/
|
|
17
|
+
function loadNodeFile() {
|
|
18
|
+
return readNodeJsonSync(NODE_JSON) || {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} patch 部分字段
|
|
23
|
+
* @returns {object} 合并后写盘
|
|
24
|
+
*/
|
|
25
|
+
function saveNodeFile(patch) {
|
|
26
|
+
const data = { ...loadNodeFile(), ...patch }
|
|
27
|
+
writeNodeJsonSync(NODE_JSON, data)
|
|
28
|
+
emitNodeChange('node-config-changed', { patch })
|
|
29
|
+
return data
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @returns {string} 64 位十六进制 节点种子
|
|
34
|
+
*/
|
|
35
|
+
export function ensureNodeSeed() {
|
|
36
|
+
const data = loadNodeFile()
|
|
37
|
+
const existing = String(data.nodeSeedHex || '').trim().toLowerCase()
|
|
38
|
+
if (NODE_SEED_HEX_RE.test(existing)) return existing
|
|
39
|
+
const nodeSeedHex = randomBytes(32).toString('hex')
|
|
40
|
+
saveNodeFile({ nodeSeedHex })
|
|
41
|
+
return nodeSeedHex
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {string} 本节点 64 hex nodeHash
|
|
46
|
+
*/
|
|
47
|
+
export function getNodeHash() {
|
|
48
|
+
return nodeHashFromSeed(ensureNodeSeed())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @returns {{ relayUrls: string[], batterySaver: boolean, mailbox: ReturnType<typeof normalizeMailboxSettings> }} 传输与 mailbox 配置
|
|
53
|
+
*/
|
|
54
|
+
export function getNodeTransportSettings() {
|
|
55
|
+
const data = loadNodeFile()
|
|
56
|
+
const relayUrls = Array.isArray(data.relayUrls)
|
|
57
|
+
? data.relayUrls.map(url => String(url).trim()).filter(url => url.startsWith('wss://'))
|
|
58
|
+
: []
|
|
59
|
+
const batterySaver = !!data.batterySaver
|
|
60
|
+
const mailbox = normalizeMailboxSettings(data.mailbox || {})
|
|
61
|
+
return { relayUrls, batterySaver, mailbox }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {object} patch 部分字段
|
|
66
|
+
* @returns {ReturnType<typeof getNodeTransportSettings>} 保存后的传输配置
|
|
67
|
+
*/
|
|
68
|
+
export function saveNodeTransportSettings(patch) {
|
|
69
|
+
const data = loadNodeFile()
|
|
70
|
+
if (patch.batterySaver != null) data.batterySaver = !!patch.batterySaver
|
|
71
|
+
if (patch.relayUrls)
|
|
72
|
+
data.relayUrls = patch.relayUrls.map(url => String(url).trim()).filter(url => url.startsWith('wss://'))
|
|
73
|
+
if (patch.mailbox)
|
|
74
|
+
data.mailbox = normalizeMailboxSettings({ ...data.mailbox, ...patch.mailbox })
|
|
75
|
+
saveNodeFile(data)
|
|
76
|
+
return getNodeTransportSettings()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 确保 node.json 存在且含 nodeSeed、mailbox 默认值。
|
|
81
|
+
* @returns {ReturnType<typeof getNodeTransportSettings> & { nodeHash: string }} 默认配置与 nodeHash
|
|
82
|
+
*/
|
|
83
|
+
export function ensureNodeDefaults() {
|
|
84
|
+
ensureNodeSeed()
|
|
85
|
+
const data = loadNodeFile()
|
|
86
|
+
if (!data.mailbox) saveNodeFile({ mailbox: normalizeMailboxSettings({}) })
|
|
87
|
+
return { ...getNodeTransportSettings(), nodeHash: getNodeHash() }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} nodeHash 64 位十六进制
|
|
92
|
+
* @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥(稳定身份锚)
|
|
93
|
+
* @returns {string | null} 操作者 entityHash
|
|
94
|
+
*/
|
|
95
|
+
export function operatorEntityHashFromKeys(nodeHash, recoveryPubKeyHex) {
|
|
96
|
+
const pub = String(recoveryPubKeyHex || '').trim().toLowerCase().replace(/^0x/iu, '')
|
|
97
|
+
if (!isHex64(nodeHash) || !isHex64(pub)) return null
|
|
98
|
+
return userEntityHashFromRecoveryPubKeyHex(nodeHash, pub)
|
|
99
|
+
}
|