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