@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,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
+ }