@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,87 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import fs from 'node:fs'
3
+ import fsp from 'node:fs/promises'
4
+ import { dirname, join } from 'node:path'
5
+ import { Readable } from 'node:stream'
6
+ import { pipeline } from 'node:stream/promises'
7
+
8
+ import { isHex64 } from '../core/hexIds.mjs'
9
+ import { getNodeDir } from '../node/instance.mjs'
10
+
11
+ /**
12
+ * @returns {string} `{nodeDir}/chunks`
13
+ */
14
+ export function chunkStoreRoot() {
15
+ return join(getNodeDir(), 'chunks')
16
+ }
17
+
18
+ /**
19
+ * @param {string} hash 64 位十六进制 ciphertextHash
20
+ * @returns {string} 块文件绝对路径
21
+ */
22
+ export function chunkStorePath(hash) {
23
+ const chunkHash = String(hash).trim().toLowerCase()
24
+ if (!isHex64(chunkHash)) throw new Error('invalid chunk hash')
25
+ return join(chunkStoreRoot(), chunkHash.slice(0, 2), `${chunkHash}.bin`)
26
+ }
27
+
28
+ /**
29
+ * @param {string} hash 64 位十六进制
30
+ * @returns {Promise<boolean>} 本地是否存在
31
+ */
32
+ export async function hasChunk(hash) {
33
+ try {
34
+ await fsp.access(chunkStorePath(hash))
35
+ return true
36
+ }
37
+ catch {
38
+ return false
39
+ }
40
+ }
41
+
42
+ /**
43
+ * @param {string} hash 64 位十六进制
44
+ * @returns {Promise<Buffer>} 块字节
45
+ */
46
+ export async function getChunk(hash) {
47
+ return fsp.readFile(chunkStorePath(hash))
48
+ }
49
+
50
+ /**
51
+ * @param {string} hash 64 位十六进制
52
+ * @returns {import('node:fs').ReadStream} 可读流
53
+ */
54
+ export function createChunkReadStream(hash) {
55
+ return fs.createReadStream(chunkStorePath(hash))
56
+ }
57
+
58
+ /**
59
+ * @param {string} hash 64 位十六进制
60
+ * @param {string | Buffer | Uint8Array} data 块数据
61
+ * @returns {Promise<void>}
62
+ */
63
+ export async function putChunk(hash, data) {
64
+ const filePath = chunkStorePath(hash)
65
+ await fsp.mkdir(dirname(filePath), { recursive: true })
66
+ await fsp.writeFile(filePath, Buffer.from(data))
67
+ }
68
+
69
+ /**
70
+ * @param {string} hash 64 位十六进制
71
+ * @param {import('node:stream').Readable} readable 密文流
72
+ * @returns {Promise<void>}
73
+ */
74
+ export async function putChunkFromStream(hash, readable) {
75
+ const filePath = chunkStorePath(hash)
76
+ await fsp.mkdir(dirname(filePath), { recursive: true })
77
+ const writable = fs.createWriteStream(filePath)
78
+ await pipeline(readable, writable)
79
+ }
80
+
81
+ /**
82
+ * @param {string} hash 64 位十六进制
83
+ * @returns {Readable} chunk 可读流
84
+ */
85
+ export function chunkToReadable(hash) {
86
+ return createChunkReadStream(hash)
87
+ }
@@ -0,0 +1,99 @@
1
+ import { isEntityHash128 } from '../core/entity_id.mjs'
2
+ import { isHex64 } from '../core/hexIds.mjs'
3
+
4
+ /** @typedef {'plain' | 'convergent' | 'random'} CeMode */
5
+
6
+ /** @typedef {{ hash: string, size: number }} ManifestPart */
7
+
8
+ /**
9
+ * @typedef {{
10
+ * type: 'public' | 'file-master-key-wrap' | 'vault-wrap' | 'identity-wrap',
11
+ * wrappedKey?: { iv: string, ciphertext: string, authTag: string },
12
+ * groupId?: string,
13
+ * fileId?: string,
14
+ * entityHash?: string,
15
+ * keyGeneration?: number,
16
+ * }} TransferKeyDescriptor
17
+ */
18
+
19
+ /**
20
+ * @typedef {{
21
+ * ownerEntityHash: string,
22
+ * logicalPath: string,
23
+ * name: string,
24
+ * mimeType: string,
25
+ * size: number,
26
+ * contentHash: string,
27
+ * ceMode: CeMode,
28
+ * parts: ManifestPart[],
29
+ * transferKeyDescriptor: TransferKeyDescriptor,
30
+ * meta?: object,
31
+ * }} FileManifest
32
+ */
33
+
34
+ const CE_MODES = new Set(['plain', 'convergent', 'random'])
35
+ const TRANSFER_TYPES = new Set(['public', 'file-master-key-wrap', 'vault-wrap', 'identity-wrap'])
36
+
37
+ /**
38
+ * @param {unknown} input 原始对象
39
+ * @returns {FileManifest | null} 校验后的 manifest
40
+ */
41
+ export function normalizeFileManifest(input) {
42
+ if (!input || typeof input !== 'object') return null
43
+ const ownerEntityHash = String(input.ownerEntityHash || '').trim().toLowerCase()
44
+ if (!isEntityHash128(ownerEntityHash)) return null
45
+ const logicalPath = String(input.logicalPath || '').trim().replace(/^\/+/, '').replace(/\\/g, '/')
46
+ if (!logicalPath) return null
47
+ const ceMode = String(input.ceMode || 'convergent')
48
+ if (!CE_MODES.has(ceMode)) return null
49
+ const parts = Array.isArray(input.parts)
50
+ ? input.parts
51
+ .map(part => ({ hash: String(part?.hash || '').trim().toLowerCase(), size: Number(part?.size) || 0 }))
52
+ .filter(part => isHex64(part.hash))
53
+ : []
54
+ if (!parts.length) return null
55
+ const transferKeyDescriptor = normalizeTransferKeyDescriptor(input.transferKeyDescriptor)
56
+ if (!transferKeyDescriptor) return null
57
+ return {
58
+ ownerEntityHash,
59
+ logicalPath,
60
+ name: String(input.name || logicalPath.split('/').pop() || 'file'),
61
+ mimeType: String(input.mimeType || 'application/octet-stream'),
62
+ size: Number(input.size) || 0,
63
+ contentHash: String(input.contentHash || '').trim().toLowerCase(),
64
+ ceMode: /** @type {CeMode} */ ceMode,
65
+ parts,
66
+ transferKeyDescriptor,
67
+ meta: input.meta?.constructor === Object ? { ...input.meta } : undefined,
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {unknown} input 描述符
73
+ * @returns {TransferKeyDescriptor | null} 校验后的传输密钥描述符
74
+ */
75
+ export function normalizeTransferKeyDescriptor(input) {
76
+ if (!input || typeof input !== 'object') return null
77
+ const type = String(input.type || '').trim()
78
+ if (!TRANSFER_TYPES.has(type)) return null
79
+ /** @type {TransferKeyDescriptor} */
80
+ const out = { type: /** @type {TransferKeyDescriptor['type']} */ type }
81
+ if (input.wrappedKey?.constructor === Object)
82
+ out.wrappedKey = {
83
+ iv: String(input.wrappedKey.iv || ''),
84
+ ciphertext: String(input.wrappedKey.ciphertext || ''),
85
+ authTag: String(input.wrappedKey.authTag || ''),
86
+ }
87
+ if (input.groupId) out.groupId = String(input.groupId).trim()
88
+ if (input.fileId) out.fileId = String(input.fileId).trim()
89
+ if (input.entityHash) out.entityHash = String(input.entityHash).trim().toLowerCase()
90
+ if (input.keyGeneration != null) out.keyGeneration = Number(input.keyGeneration) || 0
91
+ return out
92
+ }
93
+
94
+ /**
95
+ * @returns {TransferKeyDescriptor} public 描述符
96
+ */
97
+ export function publicTransferKeyDescriptor() {
98
+ return { type: 'public' }
99
+ }
@@ -0,0 +1,87 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { createHash } from 'node:crypto'
3
+
4
+ import {
5
+ decryptConvergentCiphertext,
6
+ decryptRandomCiphertext,
7
+ unwrapContentKey,
8
+ } from '../crypto/key.mjs'
9
+
10
+ /**
11
+ * @typedef {import('./manifest.mjs').FileManifest} FileManifest
12
+ * @typedef {import('./manifest.mjs').TransferKeyDescriptor} TransferKeyDescriptor
13
+ */
14
+
15
+ /**
16
+ * @param {TransferKeyDescriptor} descriptor 传递密钥描述符
17
+ * @param {FileManifest} manifest manifest
18
+ * @param {{ getGroupFileMasterKey?: (groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (entityHash: string) => Promise<Buffer | string | null> }} deps 密钥源
19
+ * @returns {Promise<Buffer | null>} contentKey;plain/convergent 返回 null(按 contentHash 派生)
20
+ */
21
+ export async function resolveContentKey(descriptor, manifest, deps = {}) {
22
+ const type = descriptor?.type || 'public'
23
+ if (type === 'public' || manifest.ceMode === 'plain' || manifest.ceMode === 'convergent')
24
+ return null
25
+
26
+ if (type === 'file-master-key-wrap') {
27
+ const groupId = descriptor.groupId
28
+ const fileId = descriptor.fileId
29
+ if (!groupId || !fileId || !descriptor.wrappedKey || !deps.getGroupFileMasterKey) return null
30
+ const groupKey = await deps.getGroupFileMasterKey(String(groupId), descriptor.keyGeneration)
31
+ if (!groupKey) return null
32
+ return unwrapContentKey(descriptor.wrappedKey, groupKey, fileId)
33
+ }
34
+
35
+ if (type === 'vault-wrap') {
36
+ const entityHash = descriptor.entityHash
37
+ const fileId = descriptor.fileId
38
+ if (!entityHash || !fileId || !descriptor.wrappedKey || !deps.getVaultMasterKey) return null
39
+ const vaultKey = await deps.getVaultMasterKey(String(entityHash))
40
+ if (!vaultKey) return null
41
+ return unwrapContentKey(descriptor.wrappedKey, vaultKey, fileId)
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * @param {Buffer | Uint8Array} encryptedPartBytes 密文块
49
+ * @param {FileManifest} manifest manifest
50
+ * @param {Buffer | null} contentKey random 模式密钥
51
+ * @returns {Buffer | null} 明文
52
+ */
53
+ export function decryptPart(encryptedPartBytes, manifest, contentKey) {
54
+ if (manifest.ceMode === 'plain')
55
+ return Buffer.from(encryptedPartBytes)
56
+
57
+ if (manifest.ceMode === 'convergent')
58
+ return decryptConvergentCiphertext(encryptedPartBytes, manifest.contentHash)
59
+
60
+ if (manifest.ceMode === 'random' && contentKey)
61
+ return decryptRandomCiphertext(encryptedPartBytes, contentKey, manifest.contentHash)
62
+
63
+ return null
64
+ }
65
+
66
+ /**
67
+ * @param {FileManifest} manifest manifest
68
+ * @param {Array<Buffer | Uint8Array>} partBytes 按序密文块
69
+ * @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} deps 密钥源
70
+ * @returns {Promise<Buffer | null>} 完整明文
71
+ */
72
+ export async function assembleManifestPlaintext(manifest, partBytes, deps = {}) {
73
+ if (partBytes.length !== manifest.parts.length) return null
74
+ const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, deps)
75
+ /** @type {Buffer[]} */
76
+ const plains = []
77
+ for (let index = 0; index < manifest.parts.length; index++) {
78
+ const plain = decryptPart(partBytes[index], manifest, contentKey)
79
+ if (!plain) return null
80
+ plains.push(plain)
81
+ }
82
+ const merged = Buffer.concat(plains)
83
+ if (manifest.contentHash)
84
+ if (createHash('sha256').update(merged).digest('hex') !== manifest.contentHash.toLowerCase()) return null
85
+
86
+ return merged
87
+ }
@@ -0,0 +1,108 @@
1
+ /** @type {Map<string, { getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }>} */
2
+ const transferDepsByOwner = new Map()
3
+
4
+ /** @type {Map<string, (replicaUsername: string, manifest: import('./manifest.mjs').FileManifest) => Promise<Buffer | null>>} */
5
+ const dagPlaintextReadersByOwner = new Map()
6
+
7
+ /**
8
+ * @param {string} ownerId 注册方
9
+ * @param {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} deps 密钥源
10
+ * @returns {void}
11
+ */
12
+ export function registerTransferKeyDeps(ownerId, deps) {
13
+ const key = String(ownerId)
14
+ const prev = transferDepsByOwner.get(key) || {}
15
+ transferDepsByOwner.set(key, { ...prev, ...deps })
16
+ }
17
+
18
+ /**
19
+ * @param {string} ownerId 注册方
20
+ * @param {(replicaUsername: string, manifest: import('./manifest.mjs').FileManifest) => Promise<Buffer | null>} reader dagParts 明文读取
21
+ * @returns {void}
22
+ */
23
+ export function registerDagManifestPlaintextReader(ownerId, reader) {
24
+ dagPlaintextReadersByOwner.set(String(ownerId), reader)
25
+ }
26
+
27
+ /** @type {Array<{ ownerId: string, match: (manifest: import('./manifest.mjs').FileManifest) => boolean }>} */
28
+ const manifestOwnerMatchers = []
29
+
30
+ /**
31
+ * @param {string} ownerId 注册方
32
+ * @param {(manifest: import('./manifest.mjs').FileManifest) => boolean} match manifest 匹配谓词
33
+ * @returns {void}
34
+ */
35
+ export function registerManifestOwnerMatcher(ownerId, match) {
36
+ manifestOwnerMatchers.push({ ownerId: String(ownerId), match })
37
+ }
38
+
39
+ /**
40
+ * @param {string} ownerId 注册方
41
+ * @returns {void}
42
+ */
43
+ export function unregisterManifestOwnerMatchers(ownerId) {
44
+ const id = String(ownerId)
45
+ for (let i = manifestOwnerMatchers.length - 1; i >= 0; i--)
46
+ if (manifestOwnerMatchers[i].ownerId === id) manifestOwnerMatchers.splice(i, 1)
47
+ }
48
+
49
+ /**
50
+ * @param {string} ownerId 注册方
51
+ * @returns {void}
52
+ */
53
+ export function unregisterTransferKeyDeps(ownerId) {
54
+ const key = String(ownerId)
55
+ transferDepsByOwner.delete(key)
56
+ dagPlaintextReadersByOwner.delete(key)
57
+ unregisterManifestOwnerMatchers(key)
58
+ }
59
+
60
+ /** @returns {void} */
61
+ export function clearTransferKeyRegistry() {
62
+ transferDepsByOwner.clear()
63
+ dagPlaintextReadersByOwner.clear()
64
+ manifestOwnerMatchers.length = 0
65
+ }
66
+
67
+ /**
68
+ * 从 manifest 推断 transfer key 注册方 id(按 registerManifestOwnerMatcher 注册顺序匹配)。
69
+ * @param {import('./manifest.mjs').FileManifest} manifest 清单
70
+ * @returns {string | null} 所有者 id
71
+ */
72
+ export function resolveManifestTransferOwnerId(manifest) {
73
+ for (const { ownerId, match } of manifestOwnerMatchers)
74
+ if (match(manifest)) return ownerId
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * @param {string} [ownerId] 显式注册方;省略时按 manifest 推断
80
+ * @param {import('./manifest.mjs').FileManifest} [manifest] 用于推断 ownerId
81
+ * @returns {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} 依赖
82
+ */
83
+ export function resolveTransferKeyDeps(ownerId, manifest) {
84
+ const resolved = ownerId || (manifest ? resolveManifestTransferOwnerId(manifest) : null)
85
+ if (resolved) return transferDepsByOwner.get(String(resolved)) || {}
86
+ return {}
87
+ }
88
+
89
+ /**
90
+ * @param {string} replicaUsername 副本用户名
91
+ * @param {import('./manifest.mjs').FileManifest} manifest 清单
92
+ * @returns {Promise<Buffer | null>} 明文;无 reader 或未命中为 null
93
+ */
94
+ export async function readDagManifestPlaintext(replicaUsername, manifest) {
95
+ const ownerId = resolveManifestTransferOwnerId(manifest)
96
+ if (ownerId) {
97
+ const reader = dagPlaintextReadersByOwner.get(ownerId)
98
+ if (reader)
99
+ try {
100
+ const buf = await reader(replicaUsername, manifest)
101
+ if (buf?.length) return buf
102
+ }
103
+ catch { /* fall through */ }
104
+ return null
105
+ }
106
+
107
+ return null
108
+ }
@@ -0,0 +1,230 @@
1
+ import { sortedPrevEventIds } from '../dag/index.mjs'
2
+ import { getGovernanceAuthzTypes } from '../registries/event_type.mjs'
3
+
4
+ /**
5
+ * 事件是否存在指向本地缺失父的悬挂引用(DAG 不完整)。
6
+ * @param {Array<{ id: string, prev_event_ids?: unknown }>} events 事件列表
7
+ * @returns {boolean} 存在悬挂父则为 true
8
+ */
9
+ export function hasDanglingParents(events) {
10
+ if (!events.length) return false
11
+ const byId = new Map(events.map(event => [event.id, event]))
12
+ for (const event of events)
13
+ for (const parentId of sortedPrevEventIds(event.prev_event_ids))
14
+ if (!byId.has(parentId)) return true
15
+ return false
16
+ }
17
+
18
+ /**
19
+ * 计算 DAG 叶事件 id。
20
+ * @param {Array<{ id: string, prev_event_ids?: unknown }>} events 事件列表
21
+ * @returns {string[]} 叶 id 列表
22
+ */
23
+ export function computeDagTipIdsFromEvents(events) {
24
+ if (!events.length) return []
25
+ const referenced = new Set()
26
+ for (const event of events)
27
+ for (const parentId of sortedPrevEventIds(event.prev_event_ids))
28
+ referenced.add(parentId)
29
+
30
+ const tips = []
31
+ for (const event of events)
32
+ if (event.id && !referenced.has(event.id)) tips.push(event.id)
33
+ return tips
34
+ }
35
+
36
+ /**
37
+ * 从 tip 沿父指针闭包祖先 id。
38
+ * @param {string} tipId 叶事件 id
39
+ * @param {Map<string, { id: string, prev_event_ids?: unknown }>} byId id→事件
40
+ * @returns {Set<string>} 祖先 id 集
41
+ */
42
+ export function ancestorClosureFromTip(tipId, byId) {
43
+ const out = new Set()
44
+ const stack = [tipId]
45
+ while (stack.length) {
46
+ const id = stack.pop()
47
+ if (!id || out.has(id)) continue
48
+ out.add(id)
49
+ const event = byId.get(id)
50
+ if (!event) continue
51
+ for (const parentId of sortedPrevEventIds(event.prev_event_ids))
52
+ if (byId.has(parentId)) stack.push(parentId)
53
+ }
54
+ return out
55
+ }
56
+
57
+ /**
58
+ * 构建 id → 子事件 id 列表(仅含图内边)。
59
+ * @param {Map<string, { id: string, prev_event_ids?: unknown }>} byId id→事件
60
+ * @returns {Map<string, string[]>} 父 id → 子 id 列表
61
+ */
62
+ export function buildDagChildrenMap(byId) {
63
+ /** @type {Map<string, string[]>} */
64
+ const children = new Map()
65
+ for (const event of byId.values())
66
+ for (const parentId of sortedPrevEventIds(event.prev_event_ids)) {
67
+ if (!byId.has(parentId)) continue
68
+ const list = children.get(parentId)
69
+ if (list) list.push(event.id)
70
+ else children.set(parentId, [event.id])
71
+ }
72
+
73
+ return children
74
+ }
75
+
76
+ /**
77
+ * 从根沿子指针正向闭包(checkpoint 之后保留的后缀)。
78
+ * @param {string} rootId 根事件 id(通常为 checkpoint_event_id)
79
+ * @param {Map<string, { id: string, prev_event_ids?: unknown }>} byId id→事件
80
+ * @returns {Set<string>} 根及其后代 id
81
+ */
82
+ export function descendantClosureFromTip(rootId, byId) {
83
+ if (!rootId || !byId.has(rootId)) return new Set()
84
+ const children = buildDagChildrenMap(byId)
85
+ const out = new Set()
86
+ const stack = [rootId]
87
+ while (stack.length) {
88
+ const id = stack.pop()
89
+ if (!id || out.has(id)) continue
90
+ out.add(id)
91
+ for (const childId of children.get(id) || []) stack.push(childId)
92
+ }
93
+ return out
94
+ }
95
+
96
+ /**
97
+ * 事件是否在 root 的后代闭包中(含 root)。
98
+ * @param {string} eventId 事件 id
99
+ * @param {string} rootId 根 id
100
+ * @param {Map<string, object>} byId id→事件
101
+ * @returns {boolean} 是否为 root 的后代(含 root 自身)
102
+ */
103
+ export function isDescendantOfTip(eventId, rootId, byId) {
104
+ return descendantClosureFromTip(rootId, byId).has(eventId)
105
+ }
106
+
107
+ /**
108
+ * @param {string} tipId 叶 id
109
+ * @param {Map<string, object>} byId id→事件
110
+ * @param {Record<string, number>} reputationBySender 发送方主观信誉
111
+ * @returns {number} 该叶上治理事件信誉加权和
112
+ */
113
+ function governanceAuthzScoreForTip(tipId, byId, reputationBySender) {
114
+ let score = 0
115
+ for (const eventId of ancestorClosureFromTip(tipId, byId)) {
116
+ const event = byId.get(eventId)
117
+ if (!event || !getGovernanceAuthzTypes().has(event.type)) continue
118
+ const sender = String(event.sender).trim().toLowerCase()
119
+ if (sender) score += Number(reputationBySender[sender] ?? 0)
120
+ }
121
+ return score
122
+ }
123
+
124
+ /**
125
+ * 纯客观选支:仅统计治理事件数量,同分按 tipId 字典序(不读本地信誉)。
126
+ * @param {string[]} tips 当前叶 id 列表
127
+ * @param {Map<string, object>} byId id→事件
128
+ * @returns {string | null} 选定 tip
129
+ */
130
+ export function selectConsensusBranchTip(tips, byId) {
131
+ if (!tips.length) return null
132
+ if (tips.length === 1) return tips[0]
133
+ let best = tips[0]
134
+ let bestScore = -Infinity
135
+ for (const tip of tips) {
136
+ let score = 0
137
+ for (const eventId of ancestorClosureFromTip(tip, byId)) {
138
+ const event = byId.get(eventId)
139
+ if (event && getGovernanceAuthzTypes().has(event.type)) score++
140
+ }
141
+ if (score > bestScore || (score === bestScore && tip > best)) {
142
+ bestScore = score
143
+ best = tip
144
+ }
145
+ }
146
+ return best
147
+ }
148
+
149
+ /**
150
+ * 主观选支(仅 UI 预览 `localViewBranchTip`;checkpoint/联邦/ACL 必须用 consensus)。
151
+ * @param {string[]} tips 当前叶 id 列表
152
+ * @param {Map<string, object>} byId id→事件
153
+ * @param {Record<string, number>} [reputationBySender] 发送方主观信誉
154
+ * @param {string | null} [preferredTipId] 用户显式选支
155
+ * @returns {string | null} 选定 tip
156
+ */
157
+ export function selectAuthzBranchTip(tips, byId, reputationBySender = {}, preferredTipId = null) {
158
+ if (!tips.length) return null
159
+ if (preferredTipId && tips.includes(preferredTipId)) return preferredTipId
160
+ if (tips.length === 1) return tips[0]
161
+
162
+ let best = tips[0]
163
+ let bestScore = -Infinity
164
+ for (const tip of tips) {
165
+ const score = governanceAuthzScoreForTip(tip, byId, reputationBySender)
166
+ if (score > bestScore || (score === bestScore && tip > best)) {
167
+ bestScore = score
168
+ best = tip
169
+ }
170
+ }
171
+ return best
172
+ }
173
+
174
+ /**
175
+ * 为每个 DAG 叶计算治理事件信誉加权和(§8 选支可视化)。
176
+ * @param {string[]} tips 叶 id 列表
177
+ * @param {Map<string, object>} byId id→事件
178
+ * @param {Record<string, number>} [reputationBySender] 发送方主观信誉
179
+ * @returns {Record<string, number>} tipId → 分数
180
+ */
181
+ export function computeTipAuthzScores(tips, byId, reputationBySender = {}) {
182
+ /** @type {Record<string, number>} */
183
+ const out = {}
184
+ for (const tip of tips)
185
+ out[tip] = governanceAuthzScoreForTip(tip, byId, reputationBySender)
186
+ return out
187
+ }
188
+
189
+ /**
190
+ * 为每个 DAG 叶计算客观治理计数分(用于共识分支可视化)。
191
+ * @param {string[]} tips 叶 id 列表
192
+ * @param {Map<string, object>} byId id→事件
193
+ * @returns {Record<string, number>} tipId → 客观分
194
+ */
195
+ export function computeTipConsensusScores(tips, byId) {
196
+ /** @type {Record<string, number>} */
197
+ const out = {}
198
+ for (const tip of tips) {
199
+ let score = 0
200
+ for (const eventId of ancestorClosureFromTip(tip, byId)) {
201
+ const event = byId.get(eventId)
202
+ if (event && getGovernanceAuthzTypes().has(event.type)) score++
203
+ }
204
+ out[tip] = score
205
+ }
206
+ return out
207
+ }
208
+
209
+ /**
210
+ * 规范拓扑序中属于选定分支的事件 id。
211
+ * @param {string[]} topologicalOrder 拓扑序 id 列表
212
+ * @param {Map<string, object>} byId id→事件
213
+ * @param {string | null} branchTipId 分支尖
214
+ * @returns {string[]} 可折叠的 id 子序列
215
+ */
216
+ export function authzFoldOrderIds(topologicalOrder, byId, branchTipId) {
217
+ if (!branchTipId) return topologicalOrder
218
+ const allowed = ancestorClosureFromTip(branchTipId, byId)
219
+ return topologicalOrder.filter(id => allowed.has(id))
220
+ }
221
+
222
+ /**
223
+ * 是否存在未合并的治理分叉。
224
+ * @param {string[]} tips DAG 叶列表
225
+ * @param {string | null} branchTipId 当前权限分支尖
226
+ * @returns {boolean} 多叶且已选支时为 true
227
+ */
228
+ export function hasGovernanceFork(tips, branchTipId) {
229
+ return tips.length > 1 && !!branchTipId
230
+ }