@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,189 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { Readable } from 'node:stream'
3
+
4
+ import { FEDERATION_CHUNK_MAX_BYTES } from '../../core/constants.mjs'
5
+ import { buildFileManifestFromEnc } from '../../files/assemble.mjs'
6
+ import { encryptReadableToParts } from '../../files/assemble_stream.mjs'
7
+ import { fetchChunk } from '../../files/chunk_fetch.mjs'
8
+ import { getChunk, hasChunk, putChunk } from '../../files/chunk_store.mjs'
9
+ import { normalizeFileManifest, publicTransferKeyDescriptor } from '../../files/manifest.mjs'
10
+ import { assembleManifestPlaintext } from '../../files/transfer_key.mjs'
11
+ import { readDagManifestPlaintext, resolveTransferKeyDeps } from '../../files/transfer_key_registry.mjs'
12
+ import { getEntityStore } from '../../node/instance.mjs'
13
+
14
+
15
+ /**
16
+ * @param {string} ownerEntityHash 所有者
17
+ * @param {string} logicalPath 路径
18
+ * @returns {Promise<import('../../files/manifest.mjs').FileManifest | null>} 归一化 manifest
19
+ */
20
+ export async function loadFileManifest(ownerEntityHash, logicalPath) {
21
+ const manifest = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
22
+ return manifest ? normalizeFileManifest(manifest) : null
23
+ }
24
+
25
+ /**
26
+ * @param {import('../../files/manifest.mjs').FileManifest} manifest 清单
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function saveFileManifest(manifest) {
30
+ await getEntityStore().writeManifest(manifest.ownerEntityHash, manifest.logicalPath, manifest)
31
+ }
32
+
33
+ /**
34
+ * @param {import('../../files/manifest.mjs').FileManifest} manifest 清单
35
+ * @param {Array<Buffer | Uint8Array>} partBytes 密文块
36
+ * @returns {Promise<void>}
37
+ */
38
+ export async function storeManifestParts(manifest, partBytes) {
39
+ for (let index = 0; index < manifest.parts.length; index++)
40
+ await putChunk(manifest.parts[index].hash, partBytes[index])
41
+ }
42
+
43
+ /**
44
+ * @param {string} replicaUsername 副本用户名
45
+ * @param {import('../../files/manifest.mjs').FileManifest} manifest 清单
46
+ * @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
47
+ * @returns {Promise<Buffer | null>} 明文内容
48
+ */
49
+ export async function readManifestPlaintext(replicaUsername, manifest, opts = {}) {
50
+ const dagGroupId = manifest.meta?.groupId
51
+ if (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
52
+ const dagPlain = await readDagManifestPlaintext(replicaUsername, manifest)
53
+ if (dagPlain) return dagPlain
54
+ }
55
+
56
+ const username = opts.username || replicaUsername
57
+ /** @type {Buffer[]} */
58
+ const partBytes = []
59
+ for (const part of manifest.parts) {
60
+ let ciphertextBytes = null
61
+ if (await hasChunk(part.hash))
62
+ ciphertextBytes = await getChunk(part.hash)
63
+ else {
64
+ const fetchedChunk = await (opts.fetchChunk || fetchChunk)({
65
+ username,
66
+ ciphertextHash: part.hash,
67
+ ownerEntityHash: manifest.ownerEntityHash,
68
+ groupId: manifest.transferKeyDescriptor.groupId,
69
+ })
70
+ if (fetchedChunk) {
71
+ await putChunk(part.hash, fetchedChunk)
72
+ ciphertextBytes = Buffer.from(fetchedChunk)
73
+ }
74
+ }
75
+ if (!ciphertextBytes) return null
76
+ partBytes.push(ciphertextBytes)
77
+ }
78
+
79
+ const rawDeps = resolveTransferKeyDeps(undefined, manifest)
80
+ const deps = {
81
+ getGroupFileMasterKey: rawDeps.getGroupFileMasterKey
82
+ ? (groupId, keyGeneration) => rawDeps.getGroupFileMasterKey(replicaUsername, groupId, keyGeneration)
83
+ : undefined,
84
+ getVaultMasterKey: rawDeps.getVaultMasterKey
85
+ ? entityHash => rawDeps.getVaultMasterKey(replicaUsername, entityHash)
86
+ : undefined,
87
+ }
88
+ return assembleManifestPlaintext(manifest, partBytes, deps)
89
+ }
90
+
91
+ /**
92
+ * @param {string} replicaUsername 副本用户名
93
+ * @param {import('../../files/manifest.mjs').FileManifest} manifest 清单
94
+ * @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
95
+ * @returns {Promise<import('node:stream').Readable | null>} 明文流
96
+ */
97
+ export async function readManifestPlaintextStream(replicaUsername, manifest, opts = {}) {
98
+ const plain = await readManifestPlaintext(replicaUsername, manifest, opts)
99
+ if (!plain) return null
100
+ return Readable.from(plain)
101
+ }
102
+
103
+ /**
104
+ * @param {object} params 参数
105
+ * @param {string} params.ownerEntityHash owner
106
+ * @param {string} params.logicalPath 路径
107
+ * @param {Buffer | Uint8Array} params.plaintext 明文
108
+ * @param {string} [params.name] 文件名
109
+ * @param {string} [params.mimeType] MIME
110
+ * @param {import('../../files/manifest.mjs').CeMode} [params.ceMode] 模式
111
+ * @param {import('../../files/manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
112
+ * @param {object} [params.meta] meta
113
+ * @returns {Promise<import('../../files/manifest.mjs').FileManifest>} 写入后的 manifest
114
+ */
115
+ export async function putFileManifest(params) {
116
+ const { encryptPlaintextToParts, encryptPlaintextToMultiPartsAsync } = await import('../../files/assemble.mjs')
117
+ const {
118
+ ownerEntityHash,
119
+ logicalPath,
120
+ plaintext,
121
+ name,
122
+ mimeType,
123
+ ceMode = 'convergent',
124
+ transferKeyDescriptor,
125
+ meta,
126
+ } = params
127
+ const plainBuf = Buffer.from(plaintext)
128
+ const enc = plainBuf.length > FEDERATION_CHUNK_MAX_BYTES
129
+ ? await encryptPlaintextToMultiPartsAsync(plainBuf, ceMode)
130
+ : encryptPlaintextToParts(plainBuf, ceMode)
131
+ const manifest = buildFileManifestFromEnc({
132
+ ownerEntityHash,
133
+ logicalPath,
134
+ plaintext: plainBuf,
135
+ name,
136
+ mimeType,
137
+ ceMode,
138
+ transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
139
+ meta,
140
+ }, enc)
141
+ await storeManifestParts(manifest, enc.parts.map(part => part.raw))
142
+ await saveFileManifest(manifest)
143
+ return manifest
144
+ }
145
+
146
+ /**
147
+ * 流式写入文件(请求流 -> 加密分块 -> chunk store)。
148
+ * @param {object} params 参数
149
+ * @param {string} params.ownerEntityHash owner
150
+ * @param {string} params.logicalPath 路径
151
+ * @param {import('node:stream').Readable} params.readable 明文流
152
+ * @param {number} params.plainSize 明文字节数
153
+ * @param {string} [params.name] 文件名
154
+ * @param {string} [params.mimeType] MIME
155
+ * @param {import('../../files/manifest.mjs').CeMode} [params.ceMode] 模式
156
+ * @param {import('../../files/manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
157
+ * @param {object} [params.meta] meta
158
+ * @returns {Promise<import('../../files/manifest.mjs').FileManifest>} 写入后的 manifest
159
+ */
160
+ export async function putFileManifestFromStream(params) {
161
+ const {
162
+ ownerEntityHash,
163
+ logicalPath,
164
+ readable,
165
+ plainSize,
166
+ name,
167
+ mimeType,
168
+ ceMode = 'convergent',
169
+ transferKeyDescriptor,
170
+ meta,
171
+ } = params
172
+ const enc = await encryptReadableToParts(readable, ceMode, async part =>
173
+ putChunk(part.hash, part.raw), plainSize)
174
+ const manifest = normalizeFileManifest({
175
+ ownerEntityHash: ownerEntityHash.toLowerCase(),
176
+ logicalPath: logicalPath.replace(/^\/+/, ''),
177
+ name: name || logicalPath.split('/').pop() || 'file',
178
+ mimeType: mimeType || 'application/octet-stream',
179
+ size: plainSize,
180
+ contentHash: enc.contentHash,
181
+ ceMode,
182
+ parts: enc.parts,
183
+ transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
184
+ meta,
185
+ })
186
+ if (!manifest) throw new Error('invalid manifest')
187
+ await saveFileManifest(manifest)
188
+ return manifest
189
+ }
@@ -0,0 +1,37 @@
1
+ import { isEntityHash128 } from '../../core/entity_id.mjs'
2
+ import { assertSafeEvfsLogicalPath } from '../../core/evfs_logical_path.mjs'
3
+
4
+ /**
5
+ *
6
+ */
7
+ export const EVFS_SCHEME = 'evfs:'
8
+
9
+ /**
10
+ * @param {string} entityHash 128 位十六进制
11
+ * @param {string} logicalPath EVFS 路径
12
+ * @returns {string} evfs URI 引用
13
+ */
14
+ export function formatEvfsRef(entityHash, logicalPath) {
15
+ const eh = String(entityHash).trim().toLowerCase()
16
+ const safePath = assertSafeEvfsLogicalPath(logicalPath)
17
+ return `${EVFS_SCHEME}//${eh}/${safePath}`
18
+ }
19
+
20
+ /**
21
+ * @param {string} ref evfs URI
22
+ * @returns {{ entityHash: string, logicalPath: string } | null} 解析结果;非法为 null
23
+ */
24
+ export function parseEvfsRef(ref) {
25
+ if (typeof ref !== 'string' || !ref.startsWith(EVFS_SCHEME)) return null
26
+ try {
27
+ const url = new URL(ref)
28
+ if (url.protocol !== 'evfs:') return null
29
+ const entityHash = String(url.hostname || '').trim().toLowerCase()
30
+ const logicalPath = String(url.pathname || '').replace(/^\/+/, '')
31
+ if (!isEntityHash128(entityHash)) return null
32
+ return { entityHash, logicalPath: assertSafeEvfsLogicalPath(logicalPath) }
33
+ }
34
+ catch {
35
+ return null
36
+ }
37
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Manifest ACL 注册表:按 transferKeyDescriptor.type 路由,不硬编码业务概念。
3
+ */
4
+
5
+ /** @type {Map<string, { ownerId: string, handler: (context: ManifestAclContext, logicalPath?: string) => Promise<boolean> }>} */
6
+ const handlersByType = new Map()
7
+
8
+ /** @type {Map<string, { ownerId: string, match: (manifest: import('../../files/manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null }>} */
9
+ const matchersByOwner = new Map()
10
+
11
+ /**
12
+ * @typedef {{
13
+ * replicaUsername: string,
14
+ * ownerEntityHash: string,
15
+ * manifest: import('../../files/manifest.mjs').FileManifest,
16
+ * }} ManifestAclContext
17
+ */
18
+
19
+ /**
20
+ * @param {string} type transferKeyDescriptor.type(如 vault-wrap、file-master-key-wrap)
21
+ * @param {string} ownerId 注册方
22
+ * @param {(context: ManifestAclContext, logicalPath?: string) => Promise<boolean>} handler ACL 检查
23
+ * @returns {void}
24
+ */
25
+ export function registerManifestAcl(type, ownerId, handler) {
26
+ const key = String(type)
27
+ const owner = String(ownerId)
28
+ const existing = handlersByType.get(key)
29
+ if (existing && existing.ownerId !== owner)
30
+ throw new Error(`manifest acl handler for '${key}' already registered by '${existing.ownerId}'`)
31
+ handlersByType.set(key, { ownerId: owner, handler })
32
+ }
33
+
34
+ /**
35
+ * @param {string} type transferKeyDescriptor.type
36
+ * @param {string} ownerId 注册方
37
+ * @returns {void}
38
+ */
39
+ export function unregisterManifestAcl(type, ownerId) {
40
+ const key = String(type)
41
+ const existing = handlersByType.get(key)
42
+ if (!existing) return
43
+ if (existing.ownerId === String(ownerId))
44
+ handlersByType.delete(key)
45
+ }
46
+
47
+ /**
48
+ * @param {string} ownerId 注册方
49
+ * @param {(manifest: import('../../files/manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null} match ACL 类型匹配
50
+ * @returns {void}
51
+ */
52
+ export function registerManifestAclMatcher(ownerId, match) {
53
+ matchersByOwner.set(String(ownerId), { ownerId: String(ownerId), match })
54
+ }
55
+
56
+ /**
57
+ * @param {string} ownerId 注册方
58
+ * @returns {void}
59
+ */
60
+ export function unregisterManifestAclMatcher(ownerId) {
61
+ matchersByOwner.delete(String(ownerId))
62
+ }
63
+
64
+ /** @returns {void} */
65
+ export function clearManifestAclRegistry() {
66
+ handlersByType.clear()
67
+ matchersByOwner.clear()
68
+ }
69
+
70
+ /**
71
+ * @param {import('../../files/manifest.mjs').FileManifest | null | undefined} manifest manifest
72
+ * @param {string} ownerEntityHash 所有者
73
+ * @returns {string | null} ACL 类型
74
+ */
75
+ export function resolveManifestAclType(manifest, ownerEntityHash) {
76
+ for (const { match } of matchersByOwner.values()) {
77
+ const type = match(manifest, ownerEntityHash)
78
+ if (type) return type
79
+ }
80
+ return null
81
+ }
82
+
83
+ /**
84
+ * @param {string} type transferKeyDescriptor.type
85
+ * @param {ManifestAclContext} context 上下文
86
+ * @param {string} [logicalPath] 写路径(仅写 ACL 需要)
87
+ * @returns {Promise<boolean>} 是否允许(未注册为 false)
88
+ */
89
+ export async function checkManifestAcl(type, context, logicalPath) {
90
+ const row = handlersByType.get(String(type))
91
+ if (!row?.handler) return false
92
+ return row.handler(context, logicalPath)
93
+ }
@@ -0,0 +1,46 @@
1
+ import { getEntityStore } from '../../node/instance.mjs'
2
+
3
+ /** @type {Map<string, { replica: string | null, at: number }>} */
4
+ const replicaHostCache = new Map()
5
+
6
+ const REPLICA_HOST_CACHE_MS = 60_000
7
+
8
+ /**
9
+ * @param {string | string[]} [entityHash] owner(单个或批量)
10
+ * @returns {void}
11
+ */
12
+ export function invalidateReplicaHostCache(entityHash) {
13
+ if (Array.isArray(entityHash)) {
14
+ for (const eh of entityHash)
15
+ replicaHostCache.delete(String(eh).trim().toLowerCase())
16
+ return
17
+ }
18
+ if (entityHash) {
19
+ replicaHostCache.delete(String(entityHash).trim().toLowerCase())
20
+ return
21
+ }
22
+ replicaHostCache.clear()
23
+ }
24
+
25
+ /**
26
+ * @param {string} ownerEntityHash 所有者
27
+ * @returns {Promise<string | null>} 托管 manifest 的 replica
28
+ */
29
+ export async function findReplicaHostingEntityFiles(ownerEntityHash) {
30
+ const eh = String(ownerEntityHash).trim().toLowerCase()
31
+ const cached = replicaHostCache.get(eh)
32
+ if (cached && Date.now() - cached.at < REPLICA_HOST_CACHE_MS)
33
+ return cached.replica
34
+
35
+ const store = getEntityStore()
36
+ if (typeof store.findHostingUser === 'function') {
37
+ const replica = await store.findHostingUser(eh)
38
+ replicaHostCache.set(eh, { replica, at: Date.now() })
39
+ return replica
40
+ }
41
+
42
+ const files = await store.listEntityFiles(eh)
43
+ const replica = files.length ? '__node__' : null
44
+ replicaHostCache.set(eh, { replica, at: Date.now() })
45
+ return replica
46
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * EVFS 文件 URL 辅助。
3
+ * @param {string} entityHash 128 位十六进制
4
+ * @param {string} logicalPath EVFS 逻辑路径
5
+ * @returns {string} HTTP 地址
6
+ */
7
+ export function entityFileUrl(entityHash, logicalPath) {
8
+ const path = String(logicalPath || '').trim().replace(/^\/+/, '')
9
+ return `/api/p2p/entities/${encodeURIComponent(entityHash)}/files/${path.split('/').map(encodeURIComponent).join('/')}`
10
+ }
11
+
12
+ /**
13
+ * profile 头像 EVFS 路径 URL。
14
+ * @param {string} entityHash 128 位十六进制
15
+ * @returns {string} profile 头像 HTTP 地址
16
+ */
17
+ export function profileAvatarFileUrl(entityHash) {
18
+ return entityFileUrl(entityHash, 'profile/avatar')
19
+ }
@@ -0,0 +1,47 @@
1
+ /** @type {((username: string, entityHash: string) => string | null) | null} */
2
+ let resolveAgentCharPartName = null
3
+
4
+ /** @type {((username: string) => { entityHash: string, charPartName: string }[]) | null} */
5
+ let listLocalAgents = null
6
+
7
+ /**
8
+ * Chat Load 时注册:解析本地 agent 的 chars 目录名。
9
+ * @param {(username: string, entityHash: string) => string | null} resolver 解析函数
10
+ * @returns {void}
11
+ */
12
+ export function registerAgentCharResolver(resolver) {
13
+ resolveAgentCharPartName = resolver
14
+ }
15
+
16
+ /**
17
+ * Chat Load 时注册:枚举 replica 下本地 agent 实体。
18
+ * @param {(username: string) => { entityHash: string, charPartName: string }[]} provider 枚举函数
19
+ * @returns {void}
20
+ */
21
+ export function registerListLocalAgentsProvider(provider) {
22
+ listLocalAgents = provider
23
+ }
24
+
25
+ /** @returns {void} */
26
+ export function unregisterAgentCharResolver() {
27
+ resolveAgentCharPartName = null
28
+ }
29
+
30
+ /** @returns {void} */
31
+ export function unregisterListLocalAgentsProvider() {
32
+ listLocalAgents = null
33
+ }
34
+
35
+ /**
36
+ * @returns {((username: string, entityHash: string) => string | null) | null} 已注册解析器
37
+ */
38
+ export function getAgentCharResolver() {
39
+ return resolveAgentCharPartName
40
+ }
41
+
42
+ /**
43
+ * @returns {((username: string) => { entityHash: string, charPartName: string }[]) | null} 已注册枚举器
44
+ */
45
+ export function getListLocalAgentsProvider() {
46
+ return listLocalAgents
47
+ }
@@ -0,0 +1,141 @@
1
+ import { isEntityHash128 } from '../core/entity_id.mjs'
2
+
3
+ import { profileAvatarFileUrl } from './files/url.mjs'
4
+
5
+ /**
6
+ * @param {unknown} linkItem 链接项
7
+ * @returns {object | null} 规范化链接项或 null
8
+ */
9
+ function normalizeLink(linkItem) {
10
+ const url = String(linkItem?.url || '').trim()
11
+ if (!url) return null
12
+ return {
13
+ icon: String(linkItem.icon || '').trim(),
14
+ name: String(linkItem.name || '').trim(),
15
+ url,
16
+ }
17
+ }
18
+
19
+ /**
20
+ * @param {unknown} localizedInput 原始 localized 字段
21
+ * @returns {Record<string, object>} locale → 切片
22
+ */
23
+ export function normalizeLocalizedMap(localizedInput) {
24
+ if (!localizedInput) return {}
25
+ /** @type {Record<string, object>} */
26
+ const out = {}
27
+ for (const [key, value] of Object.entries(localizedInput)) {
28
+ const localeKey = String(key || '').trim()
29
+ if (!localeKey || !value) continue
30
+ const tags = Array.isArray(value.tags)
31
+ ? value.tags.map(t => String(t).trim()).filter(Boolean)
32
+ : undefined
33
+ const links = Array.isArray(value.links)
34
+ ? value.links.map(normalizeLink).filter(Boolean)
35
+ : undefined
36
+ /** @type {Record<string, unknown>} */
37
+ const slice = {}
38
+ if (value.name != null) slice.name = String(value.name).trim()
39
+ if (value.avatar) slice.avatar = String(value.avatar).trim()
40
+ if (value.description != null) slice.description = String(value.description)
41
+ if (value.description_markdown != null) slice.description_markdown = String(value.description_markdown)
42
+ if (value.version) slice.version = String(value.version).trim()
43
+ if (value.author) slice.author = String(value.author).trim()
44
+ if (value.home_page) slice.home_page = String(value.home_page).trim()
45
+ if (value.issue_page) slice.issue_page = String(value.issue_page).trim()
46
+ if (tags?.length) slice.tags = tags
47
+ if (links?.length) slice.links = links
48
+ if (Object.keys(slice).length) out[localeKey] = slice
49
+ }
50
+ return out
51
+ }
52
+
53
+ /**
54
+ * @param {Record<string, object>} localized 多语言切片
55
+ * @param {string[]} locales 区域设置优先级
56
+ * @returns {object | null} 匹配的语言切片
57
+ */
58
+ function pickLocalizedSlice(localized, locales) {
59
+ const keys = Object.keys(localized || {})
60
+ if (!keys.length) return null
61
+ for (const locale of locales || []) {
62
+ if (localized[locale]) return localized[locale]
63
+ const prefix = String(locale).split('-')[0]
64
+ const hit = keys.find(k => k === prefix || k.startsWith(`${prefix}-`))
65
+ if (hit) return localized[hit]
66
+ }
67
+ return localized[keys[0]]
68
+ }
69
+
70
+ /**
71
+ * @param {string} displayName 展示名
72
+ * @param {{ subjectHash?: string }} profile 资料对象
73
+ * @returns {boolean} 是否为占位展示名
74
+ */
75
+ export function isPlaceholderDisplayName(displayName, profile) {
76
+ const name = String(displayName || '').trim()
77
+ if (!name) return true
78
+ const subjectHash = String(profile?.subjectHash || '').trim().toLowerCase()
79
+ if (!subjectHash || subjectHash.length < 12) return false
80
+ const placeholder = `${subjectHash.slice(0, 8)}…${subjectHash.slice(-4)}`
81
+ return name === placeholder
82
+ }
83
+
84
+ /**
85
+ * @param {object} stored 磁盘上的 profile 对象
86
+ * @param {string[]} locales 查看者区域设置
87
+ * @param {object} infoDefaults part 默认
88
+ * @returns {object} 合并后的展示字段
89
+ */
90
+ export function resolveProfilePresentation(stored, locales, infoDefaults) {
91
+ const localized = normalizeLocalizedMap(stored?.localized)
92
+ const slice = pickLocalizedSlice(localized, locales) || {}
93
+
94
+ let name = slice.name?.trim() || infoDefaults.name
95
+ if (name && isPlaceholderDisplayName(name, stored))
96
+ name = infoDefaults.name
97
+
98
+ const description = slice.description != null
99
+ ? String(slice.description)
100
+ : infoDefaults.description
101
+ const description_markdown = slice.description_markdown != null
102
+ ? String(slice.description_markdown)
103
+ : slice.description != null ? String(slice.description) : infoDefaults.description_markdown
104
+
105
+ const tags = slice.tags?.length ? slice.tags : infoDefaults.tags
106
+ const links = slice.links?.length ? slice.links : infoDefaults.links
107
+
108
+ let avatar = slice.avatar?.trim() || infoDefaults.avatar
109
+ if (avatar && !avatar.startsWith('http') && isEntityHash128(stored?.entityHash))
110
+ avatar = profileAvatarFileUrl(stored.entityHash)
111
+ else if (!avatar && isEntityHash128(stored?.entityHash) && stored?.localized)
112
+ avatar = profileAvatarFileUrl(stored.entityHash)
113
+
114
+ return {
115
+ name: name || infoDefaults.name,
116
+ avatar: avatar || '',
117
+ description: description || '',
118
+ description_markdown: description_markdown || '',
119
+ version: slice.version?.trim() || infoDefaults.version || '',
120
+ author: slice.author?.trim() || infoDefaults.author || '',
121
+ home_page: slice.home_page?.trim() || infoDefaults.home_page || '',
122
+ issue_page: slice.issue_page?.trim() || infoDefaults.issue_page || '',
123
+ tags: [...tags],
124
+ links: [...links],
125
+ }
126
+ }
127
+
128
+ /**
129
+ * @param {Record<string, object>} localized 多语言表
130
+ * @param {string} avatarUrl 头像 URL
131
+ * @returns {Record<string, object>} 带头像 URL 的多语言表
132
+ */
133
+ export function applyAvatarToAllLocales(localized, avatarUrl) {
134
+ const keys = Object.keys(localized)
135
+ if (!keys.length) return { '': { avatar: avatarUrl } }
136
+ /** @type {Record<string, object>} */
137
+ const out = {}
138
+ for (const key of keys)
139
+ out[key] = { ...localized[key], avatar: avatarUrl }
140
+ return out
141
+ }
@@ -0,0 +1,25 @@
1
+ import { encodeEntityHash, parseEntityHash } from '../core/entity_id.mjs'
2
+ import { sha256TextHex } from '../crypto/crypto.mjs'
3
+
4
+ /** @type {string} 逻辑实体 sentinel nodeHash(非物理节点绑定) */
5
+ export const LOGICAL_ENTITY_SENTINEL_NODE_HASH = '0'.repeat(64)
6
+
7
+ /**
8
+ * @param {string} subject 完整 subject 字符串(调用方负责命名空间前缀)
9
+ * @returns {string} 128 位 logical entityHash
10
+ */
11
+ export function logicalEntityHash(subject) {
12
+ const s = String(subject || '').trim()
13
+ if (!s) throw new Error('subject required')
14
+ return encodeEntityHash(LOGICAL_ENTITY_SENTINEL_NODE_HASH, sha256TextHex(s))
15
+ }
16
+
17
+ /**
18
+ * @param {unknown} entityHash 128 位十六进制
19
+ * @returns {boolean} 是否为 logical entity(sentinel nodeHash)
20
+ */
21
+ export function isLogicalEntityHash(entityHash) {
22
+ const parsed = parseEntityHash(entityHash)
23
+ if (!parsed) return false
24
+ return parsed.nodeHash === LOGICAL_ENTITY_SENTINEL_NODE_HASH
25
+ }
@@ -0,0 +1,40 @@
1
+ /** @type {Map<string, (username: string, entityHash: string) => Promise<string | null>>} */
2
+ const resolversByOwner = new Map()
3
+
4
+ /**
5
+ * @param {string} ownerId 注册方
6
+ * @param {(username: string, entityHash: string) => Promise<string | null>} resolver logical entity id 反查
7
+ * @returns {void}
8
+ */
9
+ export function registerLogicalEntityIdResolver(ownerId, resolver) {
10
+ resolversByOwner.set(String(ownerId), resolver)
11
+ }
12
+
13
+ /**
14
+ * @param {string} ownerId 注册方
15
+ * @returns {void}
16
+ */
17
+ export function unregisterLogicalEntityIdResolver(ownerId) {
18
+ resolversByOwner.delete(String(ownerId))
19
+ }
20
+
21
+ /** @returns {void} */
22
+ export function clearLogicalEntityIdRegistry() {
23
+ resolversByOwner.clear()
24
+ }
25
+
26
+ /**
27
+ * @param {string} username 副本用户名
28
+ * @param {string} entityHash 128 位十六进制 logical entity
29
+ * @returns {Promise<string | null>} 逻辑实体 id
30
+ */
31
+ export async function resolveLogicalEntityId(username, entityHash) {
32
+ for (const resolver of resolversByOwner.values())
33
+ try {
34
+ const id = await resolver(username, entityHash)
35
+ if (id) return id
36
+ }
37
+ catch { /* next */ }
38
+
39
+ return null
40
+ }
@@ -0,0 +1,15 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
4
+
5
+ /**
6
+ * 由持久化 nodeSeed 派生 nodeHash(64 hex)。
7
+ * @param {string} seedHex 32 字节 hex
8
+ * @returns {string} 节点哈希
9
+ */
10
+ export function nodeHashFromSeed(seedHex) {
11
+ const seed = Buffer.from(String(seedHex).trim(), 'hex')
12
+ if (seed.length !== 32) throw new Error('invalid node seed')
13
+ const { publicKey } = keyPairFromSeed(seed)
14
+ return pubKeyHash(publicKey)
15
+ }