@steve02081504/fount-p2p 0.0.4 → 0.0.5

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 (43) hide show
  1. package/core/bytes_codec.mjs +0 -27
  2. package/core/constants.mjs +0 -31
  3. package/core/hexIds.mjs +0 -13
  4. package/crypto/key.mjs +14 -15
  5. package/dag/index.mjs +0 -73
  6. package/dag/storage.mjs +0 -22
  7. package/federation/entity_key_chain.mjs +0 -9
  8. package/federation/topo_order_memo.mjs +0 -15
  9. package/files/assemble.mjs +55 -39
  10. package/files/assemble_stream.mjs +96 -44
  11. package/files/chunk_provider_registry.mjs +0 -6
  12. package/files/chunk_store.mjs +1 -11
  13. package/files/evfs.mjs +64 -37
  14. package/files/manifest.mjs +11 -2
  15. package/files/manifest_acl_registry.mjs +0 -6
  16. package/files/transfer_key.mjs +12 -6
  17. package/files/transfer_key_registry.mjs +0 -7
  18. package/governance/branch.mjs +0 -11
  19. package/link/channel_mux.mjs +2 -0
  20. package/mailbox/importance.mjs +0 -33
  21. package/mailbox/settings.mjs +0 -9
  22. package/node/denylist.mjs +0 -24
  23. package/node/network.mjs +0 -51
  24. package/node/personal_block.mjs +0 -35
  25. package/node/reputation_store.mjs +0 -9
  26. package/node/retention_policy.mjs +0 -15
  27. package/package.json +1 -1
  28. package/registries/event_type.mjs +0 -12
  29. package/registries/inbound.mjs +0 -22
  30. package/reputation/engine.mjs +0 -11
  31. package/schemas/part_query.mjs +156 -0
  32. package/transport/ice_servers.mjs +0 -8
  33. package/transport/peer_identity_maps.mjs +0 -58
  34. package/transport/remote_user_room.mjs +0 -12
  35. package/transport/rtc_connection_budget.mjs +2 -0
  36. package/transport/user_room.mjs +2 -11
  37. package/trust_graph/cache.mjs +0 -13
  38. package/trust_graph/engine.mjs +0 -24
  39. package/utils/async_mutex.mjs +0 -9
  40. package/wire/part_fanout.mjs +0 -19
  41. package/wire/part_query.mjs +487 -0
  42. package/wire/part_query.tunables.json +14 -0
  43. package/wire/part_query_cache.mjs +94 -0
@@ -2,18 +2,6 @@
2
2
  * Base64 / hex / bytes 互转(无 Node 依赖,浏览器与 Node 共用)。
3
3
  */
4
4
 
5
- /**
6
- * @param {ArrayBuffer} buffer 原始二进制缓冲
7
- * @returns {string} 标准 Base64 文本
8
- */
9
- export function arrayBufferToBase64(buffer) {
10
- let binary = ''
11
- const bytes = new Uint8Array(buffer)
12
- for (let index = 0; index < bytes.byteLength; index++)
13
- binary += String.fromCharCode(bytes[index])
14
- return btoa(binary)
15
- }
16
-
17
5
  /**
18
6
  * @param {Uint8Array} u8 原始字节
19
7
  * @returns {string} 标准 Base64 文本
@@ -34,18 +22,3 @@ export function b64ToU8(b64) {
34
22
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
35
23
  return out
36
24
  }
37
-
38
- /**
39
- * @param {string} hex 十六进制文本(可带 0x 前缀,长度须为 2×nBytes)
40
- * @param {number} nBytes 目标字节数
41
- * @returns {Uint8Array} 解析得到的字节数组
42
- */
43
- export function hexToNBytes(hex, nBytes) {
44
- const normalizedHex = String(hex).replace(/^0x/iu, '').trim()
45
- if (normalizedHex.length !== nBytes * 2 || !/^[\da-f]+$/iu.test(normalizedHex))
46
- throw new RangeError('invalid hex length or characters')
47
- const out = new Uint8Array(nBytes)
48
- for (let index = 0; index < nBytes; index++)
49
- out[index] = parseInt(normalizedHex.slice(index * 2, index * 2 + 2), 16)
50
- return out
51
- }
@@ -1,33 +1,5 @@
1
- import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
2
1
  import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
3
2
 
4
- /** @type {readonly string[]} 权限键注册表顺序(运算期 BigInt 编码用) */
5
- export const PERMISSION_REGISTRY_ORDER = Object.freeze([
6
- 'VIEW_CHANNEL',
7
- 'SEND_MESSAGES',
8
- 'SEND_STICKERS',
9
- 'ADD_REACTIONS',
10
- 'MANAGE_MESSAGES',
11
- 'MANAGE_CHANNELS',
12
- 'KICK_MEMBERS',
13
- 'BAN_MEMBERS',
14
- 'MANAGE_ROLES',
15
- 'MANAGE_ADMINS',
16
- 'INVITE_MEMBERS',
17
- 'STREAM',
18
- 'CREATE_THREADS',
19
- 'UPLOAD_FILES',
20
- 'MANAGE_FILES',
21
- 'PIN_MESSAGES',
22
- 'SET_WORLD',
23
- 'ADMIN',
24
- 'BYPASS_RATE_LIMIT',
25
- ])
26
-
27
- /**
28
- * 默认晚消息冻结时间(毫秒)
29
- */
30
- export const DEFAULT_LATE_MESSAGE_FREEZE_MS = 30_000
31
3
  /**
32
4
  * 占位 `message` 无 `message_edit` 终稿时的空闲截断(毫秒);§6.4 `streamGeneratingIdleMs` 默认。
33
5
  */
@@ -46,8 +18,5 @@ export const EPOCH_CHAIN_MAX = 256
46
18
  /** 群文件经联邦复制的单块上限(字节,§10.2) */
47
19
  export const FEDERATION_CHUNK_MAX_BYTES = trustGraphTunables.federationChunkMaxBytes
48
20
 
49
- /** TrustGraph fanout 参考 Top-K(N=8 roster 时的缩放值;运行时请用 resolveFederationFanoutTopK) */
50
- export const FEDERATION_FANOUT_TOP_K = resolveFederationFanoutTopK(8, trustGraphTunables)
51
-
52
21
  /** 全局 fed_chunk_get miss 时 fanout 邻居数 */
53
22
  export const FEDERATION_CHUNK_FETCH_FANOUT_K = trustGraphTunables.federationChunkFetchFanoutK
package/core/hexIds.mjs CHANGED
@@ -58,16 +58,3 @@ export function assertHex64(value, label = 'hex64') {
58
58
  export function isSignatureHex128(value) {
59
59
  return SIGNATURE_HEX_128.test(String(value ?? '').trim())
60
60
  }
61
-
62
- /**
63
- * 外部入站专用:断言 128 位签名 hex。
64
- * @param {unknown} value 原始值
65
- * @param {string} [label='signature'] 字段名
66
- * @returns {string} 签名 hex
67
- */
68
- export function assertSignatureHex128(value, label = 'signature') {
69
- const normalized = String(value ?? '').trim().toLowerCase()
70
- if (!SIGNATURE_HEX_128.test(normalized))
71
- throw new Error(`${label} must be 128 hex characters`)
72
- return normalized
73
- }
package/crypto/key.mjs CHANGED
@@ -66,18 +66,6 @@ function toHBuf(H) {
66
66
 
67
67
  // ─── 密钥推导 ─────────────────────────────────────────────────────────────────
68
68
 
69
- /**
70
- * 推导群内两两直连额外隔离密钥:`KDF(H, "dm", sorted(a,b).join(":"))`(§11.1)
71
- * @param {string | Buffer} H 群秘密
72
- * @param {string} pubKeyHashA 第一方 pubKeyHash
73
- * @param {string} pubKeyHashB 第二方 pubKeyHash
74
- * @returns {Buffer} 32 字节 AES-256 密钥
75
- */
76
- export function derivePairKey(H, pubKeyHashA, pubKeyHashB) {
77
- const sorted = [pubKeyHashA, pubKeyHashB].sort().join(':')
78
- return kdf(toHBuf(H), 'dm', sorted)
79
- }
80
-
81
69
  /**
82
70
  * 推导群文件加密密钥:`KDF(H, "file", fileId)`(§10.3、§6.3)
83
71
  * @param {string | Buffer} H 群秘密
@@ -327,16 +315,27 @@ export function encryptConvergentPlaintext(plaintext) {
327
315
  * @returns {{ contentHash: string, ciphertextHash: string, contentKey: Buffer, raw: Buffer }} 哈希、随机密钥与密文
328
316
  */
329
317
  export function encryptRandomPlaintext(plaintext) {
318
+ const contentKey = randomBytes(32)
319
+ const enc = encryptRandomPlaintextWithKey(plaintext, contentKey)
320
+ return { ...enc, contentKey }
321
+ }
322
+
323
+ /**
324
+ * 用已有 contentKey 加密一块明文(多块 random 文件共用一把 key)。
325
+ * @param {Buffer | Uint8Array} plaintext 明文字节
326
+ * @param {Buffer | Uint8Array} contentKey 32 字节
327
+ * @returns {{ contentHash: string, ciphertextHash: string, raw: Buffer }} 哈希与密文
328
+ */
329
+ export function encryptRandomPlaintextWithKey(plaintext, contentKey) {
330
330
  const plain = Buffer.from(plaintext)
331
331
  const contentHash = createHash('sha256').update(plain).digest('hex')
332
- const contentKey = randomBytes(32)
333
332
  const iv = randomBytes(12)
334
- const cipher = createCipheriv('aes-256-gcm', contentKey, iv)
333
+ const cipher = createCipheriv('aes-256-gcm', Buffer.from(contentKey), iv)
335
334
  const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
336
335
  const authTag = cipher.getAuthTag()
337
336
  const raw = Buffer.concat([iv, authTag, ciphertext])
338
337
  const ciphertextHash = createHash('sha256').update(raw).digest('hex')
339
- return { contentHash, ciphertextHash, contentKey, raw }
338
+ return { contentHash, ciphertextHash, raw }
340
339
  }
341
340
 
342
341
  /**
package/dag/index.mjs CHANGED
@@ -4,15 +4,12 @@ import { createHash } from 'node:crypto'
4
4
 
5
5
  import { canonicalStringify } from '../core/canonical_json.mjs'
6
6
  import { HEX_ID_64 } from '../core/hexIds.mjs'
7
- import { HLC } from '../core/hlc.mjs'
8
- import { sign, verify } from '../crypto/crypto.mjs'
9
7
 
10
8
  /**
11
9
  * 重导出 64 位十六进制事件 ID 正则(`HEX_ID_64` 别名)。
12
10
  */
13
11
  export { HEX_ID_64 as EVENT_ID_HEX }
14
12
  const EVENT_ID_HEX = HEX_ID_64
15
- const HEX_PAIR = /^[\da-f]{2}$/iu
16
13
 
17
14
  /**
18
15
  * 对事件 id 列表做二叉 Merkle 根(字典序叶子,§7 checkpoint)。
@@ -241,76 +238,6 @@ export function topologicalCanonicalOrder(metas) {
241
238
  return ordered
242
239
  }
243
240
 
244
- /**
245
- * @param {string} hex 十六进制串
246
- * @param {number} expectedByteLength 期望字节长度
247
- * @returns {Uint8Array | null} 解析后的字节
248
- */
249
- function parseHexBytes(hex, expectedByteLength) {
250
- if (!hex || hex.length !== expectedByteLength * 2 || hex.length % 2 !== 0) return null
251
- const bytes = hex.match(/.{2}/g)
252
- if (!bytes?.every(pair => HEX_PAIR.test(pair))) return null
253
- return new Uint8Array(bytes.map(byte => Number.parseInt(byte, 16)))
254
- }
255
-
256
- /**
257
- * 签名事件(签名为不含 id 的 canonical body)
258
- * @param {object} event 含完整字段;可含占位 id
259
- * @param {Uint8Array} privateKey 私钥
260
- * @returns {Promise<string>} 十六进制签名
261
- */
262
- export async function signEvent(event, privateKey) {
263
- const body = eventBodyForSign(event)
264
- const signature = await sign(signPayloadBytes(body), privateKey)
265
- return Array.from(signature).map(byte => byte.toString(16).padStart(2, '0')).join('')
266
- }
267
-
268
- /**
269
- * 验证事件签名
270
- * @param {object} event 含 signature、sender 与正文字段的完整事件
271
- * @returns {Promise<boolean>} 验签是否通过
272
- */
273
- export async function verifyEventSignature(event) {
274
- try {
275
- const signature = parseHexBytes(event?.signature, 64)
276
- const publicKey = parseHexBytes(event?.sender, 32)
277
- if (!signature || !publicKey) return false
278
- return await verify(signature, signPayloadBytes(eventBodyForSign(event)), publicKey)
279
- }
280
- catch (error) {
281
- console.error('Signature verification failed:', error)
282
- return false
283
- }
284
- }
285
-
286
- /**
287
- * 创建新事件
288
- * @param {object} params 事件字段与签名私钥等构造参数
289
- * @param {string[]} [params.prev_event_ids] 父事件 id;根事件 `[]`
290
- * @returns {Promise<object>} 含 id、signature 的完整事件对象
291
- */
292
- export async function createEvent(params) {
293
- const {
294
- type, groupId, channelId, sender, charId, content, prev_event_ids, privateKey, hlc,
295
- } = params
296
-
297
- const event = {
298
- type,
299
- groupId,
300
- channelId: channelId || null,
301
- sender,
302
- charId: charId || null,
303
- timestamp: Date.now(),
304
- hlc: hlc || HLC.now(),
305
- prev_event_ids: sortedPrevEventIds(prev_event_ids),
306
- content,
307
- }
308
-
309
- event.id = computeEventId(eventBodyForSign(event))
310
- event.signature = await signEvent(event, privateKey)
311
- return event
312
- }
313
-
314
241
  /**
315
242
  * @param {object[]} events DAG 事件行
316
243
  * @returns {Array<{ id: string, prev_event_ids?: unknown, hlc?: object, node_id?: string, sender?: string }>} 拓扑 meta 列表
package/dag/storage.mjs CHANGED
@@ -196,17 +196,6 @@ export async function readJsonlTipId(filePath) {
196
196
  }
197
197
  }
198
198
 
199
- /**
200
- * 将单个 JSON 对象作为一行追加写入 JSONL(必要时创建父目录)。
201
- * @param {string} filePath 目标文件路径
202
- * @param {object} record 要序列化写入的对象
203
- * @returns {Promise<void>}
204
- */
205
- export async function appendJsonl(filePath, record) {
206
- await mkdir(dirname(filePath), { recursive: true })
207
- await appendFile(filePath, `${JSON.stringify(record)}\n`, 'utf8')
208
- }
209
-
210
199
  /**
211
200
  * 流式重写 JSONL(临时文件 + rename),避免大数组 join 的内存峰值。
212
201
  * @param {string} filePath 目标路径
@@ -244,17 +233,6 @@ export async function writeJsonlSynced(filePath, records) {
244
233
  return withAsyncMutex(jsonlMutexKey(filePath), () => writeJsonl(filePath, records))
245
234
  }
246
235
 
247
- /**
248
- * 在 per-file 互斥锁内过滤重写 JSONL。
249
- * @param {string} filePath 目标路径
250
- * @param {(row: object) => boolean} keep 保留谓词
251
- * @param {{ sanitize?: (row: object) => object }} [options] 读行净化
252
- * @returns {Promise<{ kept: number, dropped: number }>} 统计
253
- */
254
- export async function rewriteJsonlKeepingSynced(filePath, keep, options = {}) {
255
- return withAsyncMutex(jsonlMutexKey(filePath), () => rewriteJsonlKeeping(filePath, keep, options))
256
- }
257
-
258
236
  /**
259
237
  * 追加一行 JSONL 并 `fsync`。
260
238
  * @param {string} filePath 目标路径
@@ -63,15 +63,6 @@ export function resolveActiveKeyAtGeneration(keyHistory, generation) {
63
63
  return entry?.activePubKeyHex ?? null
64
64
  }
65
65
 
66
- /**
67
- * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
68
- * @returns {number} 最新代际,无则 -1
69
- */
70
- export function resolveLatestActiveGeneration(keyHistory) {
71
- if (!keyHistory?.length) return -1
72
- return Math.max(...keyHistory.map(row => row.generation))
73
- }
74
-
75
66
  /**
76
67
  * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
77
68
  * @param {number} generation 代际
@@ -1,21 +1,6 @@
1
- import { eventsToMetas, topologicalCanonicalOrder } from '../dag/index.mjs'
2
-
3
1
  /** @type {Map<string, { fp: string, order: string[] }>} */
4
2
  const memoByKey = new Map()
5
3
 
6
- /**
7
- * 进程内拓扑序 memo(key 由调用方提供,如 username:groupId)。
8
- * @param {string} memoKey 缓存键
9
- * @param {string} fingerprint 文件 stat + 事件数指纹
10
- * @param {object[]} events 全量事件
11
- * @param {{ force?: boolean }} [opts] 强制重算
12
- * @returns {string[]} 拓扑序 event id
13
- */
14
- export function resolveTopologicalOrderMemo(memoKey, fingerprint, events, opts = {}) {
15
- return resolveTopologicalOrderMemoCached(memoKey, fingerprint, () =>
16
- topologicalCanonicalOrder(eventsToMetas(events)), opts)
17
- }
18
-
19
4
  /**
20
5
  * 进程内拓扑序 memo;`resolveOrder` 可接入磁盘缓存等实现。
21
6
  * @param {string} memoKey 缓存键
@@ -1,11 +1,12 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import { createHash } from 'node:crypto'
2
+ import { createHash, randomBytes } from 'node:crypto'
3
3
  import { setImmediate } from 'node:timers'
4
4
 
5
5
  import { FEDERATION_CHUNK_MAX_BYTES } from '../core/constants.mjs'
6
6
  import {
7
7
  encryptConvergentPlaintext,
8
8
  encryptRandomPlaintext,
9
+ encryptRandomPlaintextWithKey,
9
10
  wrapContentKey,
10
11
  } from '../crypto/key.mjs'
11
12
 
@@ -48,17 +49,57 @@ function encryptionStrategyFor(ceMode) {
48
49
  return strategy
49
50
  }
50
51
 
52
+ /**
53
+ * @param {{ contentHash: string, ciphertextHash: string, raw: Buffer }} enc 分块加密结果
54
+ * @param {CeMode} ceMode 模式
55
+ * @returns {{ hash: string, size: number, raw: Buffer, contentHash?: string }} manifest part
56
+ */
57
+ function partFromEnc(enc, ceMode) {
58
+ /** @type {{ hash: string, size: number, raw: Buffer, contentHash?: string }} */
59
+ const part = { hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw }
60
+ if (ceMode === 'convergent' || ceMode === 'plain')
61
+ part.contentHash = enc.contentHash
62
+ return part
63
+ }
64
+
65
+ /**
66
+ * 加密单个分块(random 模式复用调用方提供的 contentKey)。
67
+ * @param {Buffer | Uint8Array} slice 明文分块
68
+ * @param {CeMode} ceMode 模式
69
+ * @param {Buffer | null} [contentKey] random 模式密钥
70
+ * @returns {{ hash: string, size: number, raw: Buffer, contentHash?: string }} manifest part
71
+ */
72
+ export function encryptSliceToPart(slice, ceMode, contentKey = null) {
73
+ const enc = ceMode === 'random'
74
+ ? encryptRandomPlaintextWithKey(slice, contentKey)
75
+ : encryptionStrategyFor(ceMode)(slice)
76
+ return partFromEnc(enc, ceMode)
77
+ }
78
+
79
+ /**
80
+ * @param {Array<{ hash: string, size: number, contentHash?: string }>} parts 分块
81
+ * @returns {Array<{ hash: string, size: number, contentHash?: string }>} 写入 manifest 的 parts
82
+ */
83
+ export function manifestPartsForPersist(parts) {
84
+ return parts.map(part => {
85
+ /** @type {{ hash: string, size: number, contentHash?: string }} */
86
+ const out = { hash: part.hash, size: part.size }
87
+ if (part.contentHash) out.contentHash = part.contentHash
88
+ return out
89
+ })
90
+ }
91
+
51
92
  /**
52
93
  * @param {Buffer | Uint8Array} plaintext 明文
53
94
  * @param {CeMode} ceMode 模式
54
- * @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer }>, contentKey?: Buffer }} 加密结果
95
+ * @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>, contentKey?: Buffer }} 加密结果
55
96
  */
56
97
  export function encryptPlaintextToParts(plaintext, ceMode = 'convergent') {
57
98
  const plain = Buffer.from(plaintext)
58
99
  const enc = encryptionStrategyFor(ceMode)(plain)
59
100
  return {
60
101
  contentHash: enc.contentHash,
61
- parts: [{ hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw }],
102
+ parts: [partFromEnc(enc, ceMode)],
62
103
  contentKey: enc.contentKey,
63
104
  }
64
105
  }
@@ -67,7 +108,7 @@ export function encryptPlaintextToParts(plaintext, ceMode = 'convergent') {
67
108
  * 将明文拆分为多块加密(大文件)。
68
109
  * @param {Buffer | Uint8Array} plaintext 明文
69
110
  * @param {CeMode} ceMode 模式
70
- * @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer }>, contentKey?: Buffer }} 分块加密结果
111
+ * @returns {{ contentHash: string, parts: Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>, contentKey?: Buffer }} 分块加密结果
71
112
  */
72
113
  export function encryptPlaintextToMultiParts(plaintext, ceMode = 'convergent') {
73
114
  const plain = Buffer.from(plaintext)
@@ -75,15 +116,11 @@ export function encryptPlaintextToMultiParts(plaintext, ceMode = 'convergent') {
75
116
  if (plain.length <= FEDERATION_CHUNK_MAX_BYTES)
76
117
  return encryptPlaintextToParts(plain, ceMode)
77
118
 
78
- /** @type {Array<{ hash: string, size: number, raw: Buffer }>} */
119
+ /** @type {Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>} */
79
120
  const parts = []
80
- let contentKey = null
81
- for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES) {
82
- const slice = plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES)
83
- const enc = encryptionStrategyFor(ceMode)(slice)
84
- if (ceMode === 'random') contentKey = enc.contentKey
85
- parts.push({ hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw })
86
- }
121
+ const contentKey = ceMode === 'random' ? randomBytes(32) : null
122
+ for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES)
123
+ parts.push(encryptSliceToPart(plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES), ceMode, contentKey))
87
124
  return { contentHash, parts, contentKey: contentKey || undefined }
88
125
  }
89
126
 
@@ -99,15 +136,12 @@ export async function encryptPlaintextToMultiPartsAsync(plaintext, ceMode = 'con
99
136
  if (plain.length <= FEDERATION_CHUNK_MAX_BYTES)
100
137
  return encryptPlaintextToParts(plain, ceMode)
101
138
 
102
- /** @type {Array<{ hash: string, size: number, raw: Buffer }>} */
139
+ /** @type {Array<{ hash: string, size: number, raw: Buffer, contentHash?: string }>} */
103
140
  const parts = []
104
- let contentKey = null
141
+ const contentKey = ceMode === 'random' ? randomBytes(32) : null
105
142
  for (let offset = 0; offset < plain.length; offset += FEDERATION_CHUNK_MAX_BYTES) {
106
143
  if (offset > 0) await new Promise(resolve => setImmediate(resolve))
107
- const slice = plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES)
108
- const enc = encryptionStrategyFor(ceMode)(slice)
109
- if (ceMode === 'random') contentKey = enc.contentKey
110
- parts.push({ hash: enc.ciphertextHash, size: enc.raw.length, raw: enc.raw })
144
+ parts.push(encryptSliceToPart(plain.subarray(offset, offset + FEDERATION_CHUNK_MAX_BYTES), ceMode, contentKey))
111
145
  }
112
146
  return { contentHash, parts, contentKey: contentKey || undefined }
113
147
  }
@@ -144,7 +178,7 @@ export function buildFileManifest(params) {
144
178
  size: Buffer.from(plaintext).length,
145
179
  contentHash: enc.contentHash,
146
180
  ceMode,
147
- parts: enc.parts.map(part => ({ hash: part.hash, size: part.size })),
181
+ parts: manifestPartsForPersist(enc.parts),
148
182
  transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
149
183
  meta,
150
184
  })
@@ -155,7 +189,7 @@ export function buildFileManifest(params) {
155
189
  /**
156
190
  * 由已加密分块构建 manifest(vault / file-master-key-wrap 等需自定义 transferKeyDescriptor)。
157
191
  * @param {object} params 与 buildFileManifest 相同字段(不含 plaintext 重加密)
158
- * @param {{ contentHash: string, parts: Array<{ hash: string, size: number, raw?: Buffer }> }} enc 加密结果
192
+ * @param {{ contentHash: string, parts: Array<{ hash: string, size: number, raw?: Buffer, contentHash?: string }> }} enc 加密结果
159
193
  * @returns {FileManifest} manifest
160
194
  */
161
195
  export function buildFileManifestFromEnc(params, enc) {
@@ -177,7 +211,7 @@ export function buildFileManifestFromEnc(params, enc) {
177
211
  size: Buffer.from(plaintext).length,
178
212
  contentHash: enc.contentHash,
179
213
  ceMode,
180
- parts: enc.parts.map(part => ({ hash: part.hash, size: part.size })),
214
+ parts: manifestPartsForPersist(enc.parts),
181
215
  transferKeyDescriptor: transferKeyDescriptor || publicTransferKeyDescriptor(),
182
216
  meta,
183
217
  })
@@ -185,24 +219,6 @@ export function buildFileManifestFromEnc(params, enc) {
185
219
  return manifest
186
220
  }
187
221
 
188
- /**
189
- * @param {string} groupId 群 ID
190
- * @param {Buffer} contentKey 随机 contentKey
191
- * @param {Buffer | string} H 群/vault 秘密
192
- * @param {string} fileId 文件 ID
193
- * @param {number} keyGeneration 密钥代次
194
- * @returns {import('./manifest.mjs').TransferKeyDescriptor} 传输密钥描述
195
- */
196
- export function fileMasterKeyWrapDescriptor(groupId, fileId, contentKey, H, keyGeneration = 0) {
197
- return {
198
- type: 'file-master-key-wrap',
199
- groupId,
200
- fileId,
201
- keyGeneration,
202
- wrappedKey: wrapContentKey(contentKey, H, fileId),
203
- }
204
- }
205
-
206
222
  /**
207
223
  * @param {string} entityHash 所有者
208
224
  * @param {string} fileId 文件 ID