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