@steve02081504/fount-p2p 0.0.5 → 0.0.6
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/dag/canonicalize_row.mjs +5 -5
- package/discovery/mdns.mjs +4 -4
- package/discovery/nostr.mjs +3 -3
- package/federation/chunk_fetch_pending.mjs +3 -3
- package/federation/chunk_fetch_scheduler.mjs +3 -3
- package/federation/dag_order_cache.mjs +3 -3
- package/federation/topo_order_memo.mjs +3 -3
- package/files/chunk_responder.mjs +1 -1
- package/files/evfs.mjs +26 -26
- package/files/transfer_key.mjs +9 -9
- package/files/transfer_key_registry.mjs +9 -9
- package/governance/join_pow.mjs +17 -17
- package/link/channel_mux.mjs +7 -7
- package/link/frame.mjs +5 -5
- package/link/handshake.mjs +17 -17
- package/link/link.mjs +33 -33
- package/mailbox/deliver_or_store.mjs +13 -13
- package/mailbox/wire.mjs +4 -4
- package/node/reputation_store.mjs +5 -5
- package/node/retention_policy.mjs +8 -8
- package/overlay/index.mjs +6 -6
- package/package.json +1 -1
- package/registries/inbound.mjs +8 -8
- package/rooms/scoped_link.mjs +18 -18
- package/timeline/append_core.mjs +3 -3
- package/transport/group_link_set.mjs +30 -30
- package/transport/link_registry.mjs +13 -13
- package/transport/peer_pool.mjs +4 -4
- package/transport/user_room.mjs +12 -14
- package/wire/group_part.mjs +3 -3
- package/wire/part_ingress.mjs +14 -14
- package/wire/part_query.mjs +81 -82
- package/wire/part_query_cache.mjs +7 -0
package/dag/canonicalize_row.mjs
CHANGED
|
@@ -47,19 +47,19 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
|
|
|
47
47
|
* prepare?: (event: object) => object,
|
|
48
48
|
* contentHexKeys?: ReadonlySet<string>,
|
|
49
49
|
* entityHashKeys?: ReadonlySet<string>,
|
|
50
|
-
* }} [
|
|
50
|
+
* }} [options] 各域字段集
|
|
51
51
|
* @returns {object} canonical 行
|
|
52
52
|
*/
|
|
53
|
-
export function canonicalizeSignedRow(event,
|
|
54
|
-
const out =
|
|
53
|
+
export function canonicalizeSignedRow(event, options = {}) {
|
|
54
|
+
const out = options.prepare ? options.prepare({ ...event }) : { ...event }
|
|
55
55
|
out.id = assertHex64(out.id, 'id')
|
|
56
56
|
out.sender = assertHex64(out.sender, 'sender')
|
|
57
57
|
if (Array.isArray(out.prev_event_ids))
|
|
58
58
|
out.prev_event_ids = out.prev_event_ids.map((id, index) =>
|
|
59
59
|
assertHex64(id, `prev_event_ids[${index}]`),
|
|
60
60
|
)
|
|
61
|
-
const hexKeys =
|
|
61
|
+
const hexKeys = options.contentHexKeys
|
|
62
62
|
if (out.content && hexKeys?.size)
|
|
63
|
-
out.content = canonicalizeRowContent(out.content, hexKeys,
|
|
63
|
+
out.content = canonicalizeRowContent(out.content, hexKeys, options.entityHashKeys)
|
|
64
64
|
return out
|
|
65
65
|
}
|
package/discovery/mdns.mjs
CHANGED
|
@@ -7,12 +7,12 @@ const DEFAULT_GROUP = '239.255.42.99'
|
|
|
7
7
|
/**
|
|
8
8
|
* 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
|
|
9
9
|
*
|
|
10
|
-
* @param {{ port?: number, group?: string }} [
|
|
10
|
+
* @param {{ port?: number, group?: string }} [options] 组播端口与组地址
|
|
11
11
|
* @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
|
|
12
12
|
*/
|
|
13
|
-
export function createMdnsDiscoveryProvider(
|
|
14
|
-
const port = Number(
|
|
15
|
-
const group = String(
|
|
13
|
+
export function createMdnsDiscoveryProvider(options = {}) {
|
|
14
|
+
const port = Number(options.port) || DEFAULT_PORT
|
|
15
|
+
const group = String(options.group || DEFAULT_GROUP)
|
|
16
16
|
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true })
|
|
17
17
|
let bound = false
|
|
18
18
|
let bindPromise = null
|
package/discovery/nostr.mjs
CHANGED
|
@@ -142,11 +142,11 @@ async function publishEvent(relayUrls, event) {
|
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
144
|
* 创建 Nostr discovery provider。
|
|
145
|
-
* @param {{ relayUrls?: string[] }} [
|
|
145
|
+
* @param {{ relayUrls?: string[] }} [options] 可选中继 URL 覆盖
|
|
146
146
|
* @returns {import('./index.mjs').DiscoveryProvider} Nostr 发现提供者
|
|
147
147
|
*/
|
|
148
|
-
export function createNostrDiscoveryProvider(
|
|
149
|
-
const relayUrls = mergeSignalingRelayUrls(
|
|
148
|
+
export function createNostrDiscoveryProvider(options = {}) {
|
|
149
|
+
const relayUrls = mergeSignalingRelayUrls(options.relayUrls)
|
|
150
150
|
const secretKey = randomBytes(32)
|
|
151
151
|
return {
|
|
152
152
|
id: 'nostr',
|
|
@@ -14,10 +14,10 @@ export const MAX_PENDING_CHUNK_FETCHES = 2048
|
|
|
14
14
|
* @param {string} key 唯一等待键
|
|
15
15
|
* @param {string} expectedHash 期望 64 hex 密文哈希
|
|
16
16
|
* @param {number} timeoutMs 超时毫秒
|
|
17
|
-
* @param {{ rejectOnTimeout?: boolean }} [
|
|
17
|
+
* @param {{ rejectOnTimeout?: boolean }} [options] rejectOnTimeout 时 Promise 以 Error 拒绝
|
|
18
18
|
* @returns {{ done: Promise<Uint8Array | null>, cancel: () => void }} 等待 Promise 与取消函数
|
|
19
19
|
*/
|
|
20
|
-
export function registerChunkFetchWait(key, expectedHash, timeoutMs,
|
|
20
|
+
export function registerChunkFetchWait(key, expectedHash, timeoutMs, options = {}) {
|
|
21
21
|
if (!key || pendingChunkFetches.size >= MAX_PENDING_CHUNK_FETCHES)
|
|
22
22
|
return {
|
|
23
23
|
done: Promise.resolve(null), /**
|
|
@@ -41,7 +41,7 @@ export function registerChunkFetchWait(key, expectedHash, timeoutMs, opts = {})
|
|
|
41
41
|
})
|
|
42
42
|
const timer = setTimeout(() => {
|
|
43
43
|
pendingChunkFetches.delete(key)
|
|
44
|
-
if (
|
|
44
|
+
if (options.rejectOnTimeout)
|
|
45
45
|
settle(new Error('chunk fetch timeout'))
|
|
46
46
|
else
|
|
47
47
|
settle(null)
|
|
@@ -23,11 +23,11 @@ export function assignChunksToPeers(chunkHashes, peerIds) {
|
|
|
23
23
|
* @param {Map<string, { state: ChunkFetchState, peerId?: string, attempts: number }>} table 状态表
|
|
24
24
|
* @param {string[]} chunkHashes 待拉取块
|
|
25
25
|
* @param {string[]} peerIds 可用 peer
|
|
26
|
-
* @param {{ maxAttempts?: number }} [
|
|
26
|
+
* @param {{ maxAttempts?: number }} [options] 选项
|
|
27
27
|
* @returns {{ assignments: Map<string, string>, broadcast: string[] }} 分配与需广播块
|
|
28
28
|
*/
|
|
29
|
-
export function planChunkFetches(table, chunkHashes, peerIds,
|
|
30
|
-
const maxAttempts = Math.max(1, Number(
|
|
29
|
+
export function planChunkFetches(table, chunkHashes, peerIds, options = {}) {
|
|
30
|
+
const maxAttempts = Math.max(1, Number(options.maxAttempts) || 3)
|
|
31
31
|
const assignments = assignChunksToPeers(chunkHashes, peerIds)
|
|
32
32
|
/** @type {string[]} */
|
|
33
33
|
const broadcast = []
|
|
@@ -43,12 +43,12 @@ export function mergeTopologicalOrder(cachedOrder, events) {
|
|
|
43
43
|
/**
|
|
44
44
|
* @param {object[]} events 全量事件
|
|
45
45
|
* @param {object | null} cache 磁盘缓存
|
|
46
|
-
* @param {{ forceFull?: boolean }} [
|
|
46
|
+
* @param {{ forceFull?: boolean }} [options] 选项
|
|
47
47
|
* @returns {string[]} 拓扑序 id 列表
|
|
48
48
|
*/
|
|
49
|
-
export function resolveEventTopologicalOrder(events, cache,
|
|
49
|
+
export function resolveEventTopologicalOrder(events, cache, options = {}) {
|
|
50
50
|
if (!events.length) return []
|
|
51
|
-
if (
|
|
51
|
+
if (options.forceFull)
|
|
52
52
|
return topologicalCanonicalOrder(eventsToMetas(events))
|
|
53
53
|
|
|
54
54
|
const byId = new Map(events.map(event => [event.id, event]))
|
|
@@ -6,11 +6,11 @@ const memoByKey = new Map()
|
|
|
6
6
|
* @param {string} memoKey 缓存键
|
|
7
7
|
* @param {string} fingerprint 文件 stat + 事件数指纹
|
|
8
8
|
* @param {() => string[]} resolveOrder 实际求序(含磁盘层)
|
|
9
|
-
* @param {{ force?: boolean }} [
|
|
9
|
+
* @param {{ force?: boolean }} [options] 强制重算
|
|
10
10
|
* @returns {string[]} 拓扑序 event id
|
|
11
11
|
*/
|
|
12
|
-
export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder,
|
|
13
|
-
if (!
|
|
12
|
+
export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder, options = {}) {
|
|
13
|
+
if (!options.force) {
|
|
14
14
|
const cached = memoByKey.get(memoKey)
|
|
15
15
|
if (cached?.fp === fingerprint && cached.order.length)
|
|
16
16
|
return cached.order
|
|
@@ -71,7 +71,7 @@ export function attachNodeScopeFedChunkResponder(username, wire) {
|
|
|
71
71
|
* Trystero room:注册带 requestId 的 fed_chunk_* + fed_manifest_*(TrustGraph 全局 miss)。
|
|
72
72
|
* @param {string} username 用户
|
|
73
73
|
* @param {object} room Trystero room
|
|
74
|
-
* @param {{ enqueue: (prio: number,
|
|
74
|
+
* @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站队列
|
|
75
75
|
* @param {(roomKey: string, action: string, rtcLimits: object) => boolean} [guardGet] RTC 负载守卫
|
|
76
76
|
* @param {object} [rtcLimits] RTC 限额
|
|
77
77
|
* @param {string} [roomKey] 房间键
|
package/files/evfs.mjs
CHANGED
|
@@ -10,21 +10,21 @@ import { fetchChunk } from './chunk_fetch.mjs'
|
|
|
10
10
|
import { createChunkReadStream, getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
11
11
|
import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
|
|
12
12
|
import { assembleManifestPlaintext, resolveContentKey } from './transfer_key.mjs'
|
|
13
|
-
import { readDagManifestPlaintext,
|
|
13
|
+
import { readDagManifestPlaintext, resolveTransferKeyDependencies } from './transfer_key_registry.mjs'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {string} replicaUsername 副本用户名
|
|
17
17
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
18
18
|
* @returns {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} 密钥依赖
|
|
19
19
|
*/
|
|
20
|
-
function
|
|
21
|
-
const
|
|
20
|
+
function transferKeyDependenciesForReplica(replicaUsername, manifest) {
|
|
21
|
+
const rawDependencies = resolveTransferKeyDependencies(undefined, manifest)
|
|
22
22
|
return {
|
|
23
|
-
getGroupFileMasterKey:
|
|
24
|
-
? (groupId, keyGeneration) =>
|
|
23
|
+
getGroupFileMasterKey: rawDependencies.getGroupFileMasterKey
|
|
24
|
+
? (groupId, keyGeneration) => rawDependencies.getGroupFileMasterKey(replicaUsername, groupId, keyGeneration)
|
|
25
25
|
: undefined,
|
|
26
|
-
getVaultMasterKey:
|
|
27
|
-
? entityHash =>
|
|
26
|
+
getVaultMasterKey: rawDependencies.getVaultMasterKey
|
|
27
|
+
? entityHash => rawDependencies.getVaultMasterKey(replicaUsername, entityHash)
|
|
28
28
|
: undefined,
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -32,13 +32,13 @@ function transferKeyDepsForReplica(replicaUsername, manifest) {
|
|
|
32
32
|
/**
|
|
33
33
|
* @param {string} username 拉取身份
|
|
34
34
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
35
|
-
* @param {{ fetchChunk?: Function }} [
|
|
35
|
+
* @param {{ fetchChunk?: Function }} [options] miss 拉取
|
|
36
36
|
* @returns {Promise<boolean>} 全部 part 是否已就位
|
|
37
37
|
*/
|
|
38
|
-
async function ensureManifestPartsLocal(username, manifest,
|
|
38
|
+
async function ensureManifestPartsLocal(username, manifest, options = {}) {
|
|
39
39
|
for (const part of manifest.parts) {
|
|
40
40
|
if (await hasChunk(part.hash)) continue
|
|
41
|
-
const fetchedChunk = await (
|
|
41
|
+
const fetchedChunk = await (options.fetchChunk || fetchChunk)({
|
|
42
42
|
username,
|
|
43
43
|
ciphertextHash: part.hash,
|
|
44
44
|
ownerEntityHash: manifest.ownerEntityHash,
|
|
@@ -81,46 +81,46 @@ export async function storeManifestParts(manifest, partBytes) {
|
|
|
81
81
|
/**
|
|
82
82
|
* @param {string} replicaUsername 副本用户名
|
|
83
83
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
84
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
84
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
85
85
|
* @returns {Promise<Buffer | null>} 明文内容
|
|
86
86
|
*/
|
|
87
|
-
export async function readManifestPlaintext(replicaUsername, manifest,
|
|
87
|
+
export async function readManifestPlaintext(replicaUsername, manifest, options = {}) {
|
|
88
88
|
const dagGroupId = manifest.meta?.groupId
|
|
89
89
|
if (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
|
|
90
90
|
const dagPlain = await readDagManifestPlaintext(replicaUsername, manifest)
|
|
91
91
|
if (dagPlain) return dagPlain
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const username =
|
|
95
|
-
if (!await ensureManifestPartsLocal(username, manifest,
|
|
94
|
+
const username = options.username || replicaUsername
|
|
95
|
+
if (!await ensureManifestPartsLocal(username, manifest, options)) return null
|
|
96
96
|
|
|
97
97
|
/** @type {Buffer[]} */
|
|
98
98
|
const partBytes = []
|
|
99
99
|
for (const part of manifest.parts)
|
|
100
100
|
partBytes.push(await getChunk(part.hash))
|
|
101
101
|
|
|
102
|
-
return assembleManifestPlaintext(manifest, partBytes,
|
|
102
|
+
return assembleManifestPlaintext(manifest, partBytes, transferKeyDependenciesForReplica(replicaUsername, manifest))
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* @param {string} replicaUsername 副本用户名
|
|
107
107
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
108
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
108
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
109
109
|
* @returns {Promise<import('node:stream').Readable | null>} 明文流
|
|
110
110
|
*/
|
|
111
|
-
export async function readManifestPlaintextStream(replicaUsername, manifest,
|
|
111
|
+
export async function readManifestPlaintextStream(replicaUsername, manifest, options = {}) {
|
|
112
112
|
const dagGroupId = manifest.meta?.groupId
|
|
113
113
|
if (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
|
|
114
|
-
const plain = await readManifestPlaintext(replicaUsername, manifest,
|
|
114
|
+
const plain = await readManifestPlaintext(replicaUsername, manifest, options)
|
|
115
115
|
if (!plain) return null
|
|
116
116
|
return Readable.from([plain])
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const username =
|
|
120
|
-
if (!await ensureManifestPartsLocal(username, manifest,
|
|
119
|
+
const username = options.username || replicaUsername
|
|
120
|
+
if (!await ensureManifestPartsLocal(username, manifest, options)) return null
|
|
121
121
|
|
|
122
|
-
const
|
|
123
|
-
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest,
|
|
122
|
+
const dependencies = transferKeyDependenciesForReplica(replicaUsername, manifest)
|
|
123
|
+
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, dependencies)
|
|
124
124
|
if (manifest.ceMode === 'random' && !contentKey) return null
|
|
125
125
|
|
|
126
126
|
const partStreams = manifest.parts.map(part => createChunkReadStream(part.hash))
|
|
@@ -219,16 +219,16 @@ export async function putFileManifestFromStream(params) {
|
|
|
219
219
|
* @param {string} replicaUsername 副本用户名
|
|
220
220
|
* @param {string} entityHash owner entityHash
|
|
221
221
|
* @param {string} logicalPath EVFS 逻辑路径
|
|
222
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
222
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
223
223
|
* @returns {Promise<Buffer | null>} 明文或 null
|
|
224
224
|
*/
|
|
225
|
-
export async function readPublicFile(replicaUsername, entityHash, logicalPath,
|
|
225
|
+
export async function readPublicFile(replicaUsername, entityHash, logicalPath, options = {}) {
|
|
226
226
|
const { fetchPublicManifest } = await import('./manifest_fetch.mjs')
|
|
227
227
|
const manifest = await fetchPublicManifest({
|
|
228
|
-
username:
|
|
228
|
+
username: options.username || replicaUsername,
|
|
229
229
|
ownerEntityHash: entityHash,
|
|
230
230
|
logicalPath,
|
|
231
231
|
})
|
|
232
232
|
if (!manifest) return null
|
|
233
|
-
return readManifestPlaintext(replicaUsername, manifest,
|
|
233
|
+
return readManifestPlaintext(replicaUsername, manifest, options)
|
|
234
234
|
}
|
package/files/transfer_key.mjs
CHANGED
|
@@ -15,26 +15,26 @@ import {
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {TransferKeyDescriptor} descriptor 传递密钥描述符
|
|
17
17
|
* @param {FileManifest} manifest manifest
|
|
18
|
-
* @param {{ getGroupFileMasterKey?: (groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (entityHash: string) => Promise<Buffer | string | null> }}
|
|
18
|
+
* @param {{ getGroupFileMasterKey?: (groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (entityHash: string) => Promise<Buffer | string | null> }} dependencies 密钥源
|
|
19
19
|
* @returns {Promise<Buffer | null>} contentKey;plain/convergent 返回 null(按 contentHash 派生)
|
|
20
20
|
*/
|
|
21
|
-
export async function resolveContentKey(descriptor, manifest,
|
|
21
|
+
export async function resolveContentKey(descriptor, manifest, dependencies = {}) {
|
|
22
22
|
const type = descriptor?.type || 'public'
|
|
23
23
|
if (type === 'public' || manifest.ceMode === 'plain' || manifest.ceMode === 'convergent')
|
|
24
24
|
return null
|
|
25
25
|
|
|
26
26
|
if (type === 'file-master-key-wrap') {
|
|
27
27
|
const { groupId, fileId } = descriptor
|
|
28
|
-
if (!groupId || !fileId || !descriptor.wrappedKey || !
|
|
29
|
-
const groupKey = await
|
|
28
|
+
if (!groupId || !fileId || !descriptor.wrappedKey || !dependencies.getGroupFileMasterKey) return null
|
|
29
|
+
const groupKey = await dependencies.getGroupFileMasterKey(String(groupId), descriptor.keyGeneration)
|
|
30
30
|
if (!groupKey) return null
|
|
31
31
|
return unwrapContentKey(descriptor.wrappedKey, groupKey, fileId)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (type === 'vault-wrap') {
|
|
35
35
|
const { entityHash, fileId } = descriptor
|
|
36
|
-
if (!entityHash || !fileId || !descriptor.wrappedKey || !
|
|
37
|
-
const vaultKey = await
|
|
36
|
+
if (!entityHash || !fileId || !descriptor.wrappedKey || !dependencies.getVaultMasterKey) return null
|
|
37
|
+
const vaultKey = await dependencies.getVaultMasterKey(String(entityHash))
|
|
38
38
|
if (!vaultKey) return null
|
|
39
39
|
return unwrapContentKey(descriptor.wrappedKey, vaultKey, fileId)
|
|
40
40
|
}
|
|
@@ -70,12 +70,12 @@ export function decryptPart(encryptedPartBytes, manifest, contentKey, partIndex
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {FileManifest} manifest manifest
|
|
72
72
|
* @param {Array<Buffer | Uint8Array>} partBytes 按序密文块
|
|
73
|
-
* @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }}
|
|
73
|
+
* @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} dependencies 密钥源
|
|
74
74
|
* @returns {Promise<Buffer | null>} 完整明文
|
|
75
75
|
*/
|
|
76
|
-
export async function assembleManifestPlaintext(manifest, partBytes,
|
|
76
|
+
export async function assembleManifestPlaintext(manifest, partBytes, dependencies = {}) {
|
|
77
77
|
if (partBytes.length !== manifest.parts.length) return null
|
|
78
|
-
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest,
|
|
78
|
+
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, dependencies)
|
|
79
79
|
/** @type {Buffer[]} */
|
|
80
80
|
const plains = []
|
|
81
81
|
for (let index = 0; index < manifest.parts.length; index++) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
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
|
|
2
|
+
const transferDependenciesByOwner = new Map()
|
|
3
3
|
|
|
4
4
|
/** @type {Map<string, (replicaUsername: string, manifest: import('./manifest.mjs').FileManifest) => Promise<Buffer | null>>} */
|
|
5
5
|
const dagPlaintextReadersByOwner = new Map()
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
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> }}
|
|
9
|
+
* @param {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} dependencies 密钥源
|
|
10
10
|
* @returns {void}
|
|
11
11
|
*/
|
|
12
|
-
export function
|
|
12
|
+
export function registerTransferKeyDependencies(ownerId, dependencies) {
|
|
13
13
|
const key = String(ownerId)
|
|
14
|
-
const prev =
|
|
15
|
-
|
|
14
|
+
const prev = transferDependenciesByOwner.get(key) || {}
|
|
15
|
+
transferDependenciesByOwner.set(key, { ...prev, ...dependencies })
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -50,9 +50,9 @@ export function unregisterManifestOwnerMatchers(ownerId) {
|
|
|
50
50
|
* @param {string} ownerId 注册方
|
|
51
51
|
* @returns {void}
|
|
52
52
|
*/
|
|
53
|
-
export function
|
|
53
|
+
export function unregisterTransferKeyDependencies(ownerId) {
|
|
54
54
|
const key = String(ownerId)
|
|
55
|
-
|
|
55
|
+
transferDependenciesByOwner.delete(key)
|
|
56
56
|
dagPlaintextReadersByOwner.delete(key)
|
|
57
57
|
unregisterManifestOwnerMatchers(key)
|
|
58
58
|
}
|
|
@@ -73,9 +73,9 @@ export function resolveManifestTransferOwnerId(manifest) {
|
|
|
73
73
|
* @param {import('./manifest.mjs').FileManifest} [manifest] 用于推断 ownerId
|
|
74
74
|
* @returns {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} 依赖
|
|
75
75
|
*/
|
|
76
|
-
export function
|
|
76
|
+
export function resolveTransferKeyDependencies(ownerId, manifest) {
|
|
77
77
|
const resolved = ownerId || (manifest ? resolveManifestTransferOwnerId(manifest) : null)
|
|
78
|
-
if (resolved) return
|
|
78
|
+
if (resolved) return transferDependenciesByOwner.get(String(resolved)) || {}
|
|
79
79
|
return {}
|
|
80
80
|
}
|
|
81
81
|
|
package/governance/join_pow.mjs
CHANGED
|
@@ -85,40 +85,40 @@ export function powVoluntaryBonus(achievedBits, floorBits, tunables = admissionT
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* @param {object} powSolution 客户端 solution
|
|
88
|
-
* @param {object}
|
|
89
|
-
* @param {string}
|
|
90
|
-
* @param {string}
|
|
91
|
-
* @param {string[]}
|
|
92
|
-
* @param {number} [
|
|
93
|
-
* @param {number} [
|
|
94
|
-
* @param {number} [
|
|
95
|
-
* @param {number} [
|
|
88
|
+
* @param {object} options 校验上下文
|
|
89
|
+
* @param {string} options.groupId 群 ID
|
|
90
|
+
* @param {string} options.senderNodeHash 签名者 nodeHash(须与 joiner 绑定)
|
|
91
|
+
* @param {string[]} options.knownAnchors 近期 tip / checkpoint root 列表
|
|
92
|
+
* @param {number} [options.now=Date.now()] 当前时间
|
|
93
|
+
* @param {number} [options.difficultyBits=0] 准入 floor(前导零 bit)
|
|
94
|
+
* @param {number} [options.epochMs=JOIN_POW_DEFAULT_EPOCH_MS] epoch 长度
|
|
95
|
+
* @param {number} [options.epochSkew=JOIN_POW_DEFAULT_EPOCH_SKEW] 允许 epoch 偏移
|
|
96
96
|
* @returns {{ ok: boolean, achievedBits: number }} 校验结果与实际达成 bit
|
|
97
97
|
*/
|
|
98
|
-
export function verifyJoinPow(powSolution,
|
|
99
|
-
const floorBits = Math.max(0, Math.floor(Number(
|
|
98
|
+
export function verifyJoinPow(powSolution, options) {
|
|
99
|
+
const floorBits = Math.max(0, Math.floor(Number(options.difficultyBits) || 0))
|
|
100
100
|
if (floorBits <= 0) return { ok: true, achievedBits: 0 }
|
|
101
101
|
if (!powSolution || typeof powSolution !== 'object') return { ok: false, achievedBits: 0 }
|
|
102
102
|
|
|
103
103
|
const anchorRef = String(powSolution.anchorRef ?? '').trim()
|
|
104
104
|
const { nonce } = powSolution
|
|
105
105
|
const epoch = Number(powSolution.epoch)
|
|
106
|
-
const joinerNodeHash = String(powSolution.joinerNodeHash ??
|
|
107
|
-
const senderNodeHash = String(
|
|
106
|
+
const joinerNodeHash = String(powSolution.joinerNodeHash ?? options.senderNodeHash ?? '').trim().toLowerCase()
|
|
107
|
+
const senderNodeHash = String(options.senderNodeHash ?? '').trim().toLowerCase()
|
|
108
108
|
|
|
109
109
|
if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
|
|
110
110
|
if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
|
|
111
111
|
|
|
112
|
-
const anchors = (
|
|
112
|
+
const anchors = (options.knownAnchors ?? []).map(a => String(a).trim()).filter(Boolean)
|
|
113
113
|
if (!anchors.length || !anchors.includes(anchorRef)) return { ok: false, achievedBits: 0 }
|
|
114
114
|
|
|
115
|
-
const epochMs = Math.max(60_000, Number(
|
|
116
|
-
const skew = Math.max(0, Math.floor(Number(
|
|
117
|
-
const nowEpoch = Math.floor((
|
|
115
|
+
const epochMs = Math.max(60_000, Number(options.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
|
|
116
|
+
const skew = Math.max(0, Math.floor(Number(options.epochSkew) ?? JOIN_POW_DEFAULT_EPOCH_SKEW))
|
|
117
|
+
const nowEpoch = Math.floor((options.now ?? Date.now()) / epochMs)
|
|
118
118
|
if (Math.abs(epoch - nowEpoch) > skew) return { ok: false, achievedBits: 0 }
|
|
119
119
|
|
|
120
120
|
const hash = computeJoinPowHash({
|
|
121
|
-
groupId:
|
|
121
|
+
groupId: options.groupId,
|
|
122
122
|
anchorRef,
|
|
123
123
|
joinerNodeHash,
|
|
124
124
|
epoch,
|
package/link/channel_mux.mjs
CHANGED
|
@@ -110,15 +110,15 @@ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CH
|
|
|
110
110
|
/**
|
|
111
111
|
* 订阅 bufferedamountlow 事件,返回取消订阅函数。
|
|
112
112
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
113
|
-
* @param {() => void}
|
|
113
|
+
* @param {() => void} callback 低水位回调
|
|
114
114
|
* @returns {() => void} 取消订阅函数
|
|
115
115
|
*/
|
|
116
|
-
export function onBufferedAmountLow(channel,
|
|
116
|
+
export function onBufferedAmountLow(channel, callback) {
|
|
117
117
|
/**
|
|
118
118
|
* bufferedamountlow 事件处理函数。
|
|
119
119
|
* @returns {void}
|
|
120
120
|
*/
|
|
121
|
-
const handler = () =>
|
|
121
|
+
const handler = () => callback()
|
|
122
122
|
channel.addEventListener?.('bufferedamountlow', handler)
|
|
123
123
|
channel.onbufferedamountlow = handler
|
|
124
124
|
if (channel.bufferedAmountLow?.subscribe)
|
|
@@ -131,12 +131,12 @@ export function onBufferedAmountLow(channel, cb) {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* 创建带优先级队列的双通道发送器(control/bulk)。
|
|
134
|
-
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }}
|
|
134
|
+
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }} options 通道访问与高水位配置
|
|
135
135
|
* @returns {{ enqueue: (action: string, bytes: Uint8Array, preferredChannel?: 'control' | 'bulk') => void, flush: (channelName?: 'control' | 'bulk') => void, pending: () => { control: number, bulk: number }, clear: () => void }} 发送队列 API
|
|
136
136
|
*/
|
|
137
|
-
export function createChannelSendQueues(
|
|
138
|
-
const { getChannel } =
|
|
139
|
-
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(
|
|
137
|
+
export function createChannelSendQueues(options) {
|
|
138
|
+
const { getChannel } = options
|
|
139
|
+
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(options.highWatermarkBytes) || CHANNEL_HIGH_WATERMARK_BYTES)
|
|
140
140
|
/** @type {{ control: Array<{ priority: number, seq: number, bytes: Uint8Array }>, bulk: Array<{ priority: number, seq: number, bytes: Uint8Array }> }} */
|
|
141
141
|
const queues = { control: [], bulk: [] }
|
|
142
142
|
/** @type {{ control: boolean, bulk: boolean }} */
|
package/link/frame.mjs
CHANGED
|
@@ -150,13 +150,13 @@ function concatChunks(chunks) {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* 创建分片消息重组器。
|
|
153
|
-
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [
|
|
153
|
+
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [options] 大小、并发分片数与超时配置
|
|
154
154
|
* @returns {{ push: (frame: Uint8Array | ArrayBuffer | ArrayBufferView, now?: number) => Uint8Array | null, prune: (now?: number) => string[], clear: () => void, size: () => number }} 重组器 API
|
|
155
155
|
*/
|
|
156
|
-
export function createReassembler(
|
|
157
|
-
const maxMessageBytes = Math.max(1024, Number(
|
|
158
|
-
const maxPartials = Math.max(1, Number(
|
|
159
|
-
const partialTimeoutMs = Math.max(1000, Number(
|
|
156
|
+
export function createReassembler(options = {}) {
|
|
157
|
+
const maxMessageBytes = Math.max(1024, Number(options.maxMessageBytes) || DEFAULT_MAX_MESSAGE_BYTES)
|
|
158
|
+
const maxPartials = Math.max(1, Number(options.maxPartials) || DEFAULT_MAX_PARTIAL_MESSAGES)
|
|
159
|
+
const partialTimeoutMs = Math.max(1000, Number(options.partialTimeoutMs) || DEFAULT_PARTIAL_TIMEOUT_MS)
|
|
160
160
|
/** @type {Map<string, { total: number, chunks: Uint8Array[], seen: boolean[], bytes: number, firstSeenAt: number, lastSeenAt: number }>} */
|
|
161
161
|
const partials = new Map()
|
|
162
162
|
|
package/link/handshake.mjs
CHANGED
|
@@ -34,18 +34,18 @@ export function buildAuthMessage(peerNonce, localFingerprint, localNodeHash) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* 构造 link hello 握手包。
|
|
37
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [
|
|
37
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
|
|
38
38
|
* @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
|
|
39
39
|
*/
|
|
40
|
-
export function buildHello(
|
|
40
|
+
export function buildHello(options = {}) {
|
|
41
41
|
let publicKey = null
|
|
42
|
-
if (!
|
|
42
|
+
if (!options.nodeHash || !options.nodePubKey) {
|
|
43
43
|
const derived = keyPairFromSeed(Buffer.from(ensureNodeSeed(), 'hex'))
|
|
44
44
|
publicKey = derived.publicKey
|
|
45
45
|
}
|
|
46
|
-
const nodeHash = normalizeHex64(
|
|
47
|
-
const nodePubKey = normalizeHex64(
|
|
48
|
-
const nonce = normalizeHex64(
|
|
46
|
+
const nodeHash = normalizeHex64(options.nodeHash || getNodeHash())
|
|
47
|
+
const nodePubKey = normalizeHex64(options.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
48
|
+
const nonce = normalizeHex64(options.nonce || randomBytes(32).toString('hex'))
|
|
49
49
|
if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce))
|
|
50
50
|
throw new Error('p2p: invalid hello fields')
|
|
51
51
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
@@ -57,15 +57,15 @@ export function buildHello(opts = {}) {
|
|
|
57
57
|
* 对 link auth 消息签名。
|
|
58
58
|
* @param {string} peerNonce 对端 hello 中的 nonce
|
|
59
59
|
* @param {string} localFingerprint 本地 DTLS fingerprint
|
|
60
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [
|
|
60
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [options] 签名密钥与 nodeHash 覆盖
|
|
61
61
|
* @returns {Promise<{ sig: string }>} hex 签名
|
|
62
62
|
*/
|
|
63
|
-
export async function buildAuth(peerNonce, localFingerprint,
|
|
64
|
-
const seed =
|
|
65
|
-
? Buffer.from(
|
|
63
|
+
export async function buildAuth(peerNonce, localFingerprint, options = {}) {
|
|
64
|
+
const seed = options.secretKey
|
|
65
|
+
? Buffer.from(options.secretKey)
|
|
66
66
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
67
67
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
68
|
-
const nodeHash = normalizeHex64(
|
|
68
|
+
const nodeHash = normalizeHex64(options.nodeHash || pubKeyHash(publicKey))
|
|
69
69
|
if (nodeHash !== pubKeyHash(publicKey))
|
|
70
70
|
throw new Error('p2p: auth nodeHash does not match secretKey')
|
|
71
71
|
const message = buildAuthMessage(peerNonce, localFingerprint, nodeHash)
|
|
@@ -133,16 +133,16 @@ export function buildAdvertMessage(topic, ts, nodeHash) {
|
|
|
133
133
|
* 构造带签名的 discovery advert。
|
|
134
134
|
* @param {string} topic 广播主题
|
|
135
135
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
136
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string } | null} [
|
|
136
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string } | null} [options] 签名身份,省略则用本地节点
|
|
137
137
|
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string }>} 签名 advert
|
|
138
138
|
*/
|
|
139
|
-
export async function buildSignedAdvert(topic, ts = Date.now(),
|
|
140
|
-
const seed =
|
|
141
|
-
? Buffer.from(
|
|
139
|
+
export async function buildSignedAdvert(topic, ts = Date.now(), options = null) {
|
|
140
|
+
const seed = options?.secretKey
|
|
141
|
+
? Buffer.from(options.secretKey)
|
|
142
142
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
143
143
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
144
|
-
const nodeHash = normalizeHex64(
|
|
145
|
-
const nodePubKey = normalizeHex64(
|
|
144
|
+
const nodeHash = normalizeHex64(options?.nodeHash || pubKeyHash(publicKey))
|
|
145
|
+
const nodePubKey = normalizeHex64(options?.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
146
146
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
147
147
|
throw new Error('p2p: advert nodePubKey does not match nodeHash')
|
|
148
148
|
const message = buildAdvertMessage(topic, ts, nodeHash)
|