@steve02081504/fount-p2p 0.0.2 → 0.0.4
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 +1 -1
- package/{entity → core}/logical_entity.mjs +2 -1
- package/federation/manifest_fetch_pending.mjs +90 -0
- package/{entity/files → files}/acl.mjs +3 -3
- package/files/chunk_fetch.mjs +2 -33
- package/files/chunk_responder.mjs +60 -2
- package/{entity/files → files}/evfs.mjs +39 -21
- package/{entity/files → files}/evfs_ref.mjs +3 -5
- package/files/fetch_fanout.mjs +38 -0
- package/{entity/files → files}/manifest_acl_registry.mjs +4 -4
- package/files/manifest_fetch.mjs +97 -0
- package/files/public_manifest.mjs +153 -0
- package/link/channel_mux.mjs +2 -0
- package/node/identity.mjs +33 -2
- package/node/personal_block.mjs +1 -1
- package/package.json +1 -3
- package/transport/rtc_connection_budget.mjs +2 -0
- package/entity/files/replica_host_cache.mjs +0 -46
- package/entity/files/url.mjs +0 -19
- package/entity/hosting_registry.mjs +0 -47
- package/entity/localized_core.mjs +0 -141
- package/entity/logical_entity_id_registry.mjs +0 -40
- package/entity/node_hash.mjs +0 -15
- package/entity/presentation_registry.mjs +0 -44
- package/entity/profile.mjs +0 -256
- package/entity/replica.mjs +0 -20
- package/entity/session_snapshot_registry.mjs +0 -38
- package/registries/p2p_viewer.mjs +0 -35
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`,
|
|
|
34
34
|
| L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
|
|
35
35
|
| L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
|
|
36
36
|
| L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery, RTC links, rooms |
|
|
37
|
-
| L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files
|
|
37
|
+
| L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/` | Federation, store-and-forward, DAG, EVFS |
|
|
38
38
|
| — | `registries/` | Pluggable registries (event type, part path, room provider, …) |
|
|
39
39
|
|
|
40
40
|
Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscoveryProvider`, …).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { encodeEntityHash, parseEntityHash } from '../core/entity_id.mjs'
|
|
2
1
|
import { sha256TextHex } from '../crypto/crypto.mjs'
|
|
3
2
|
|
|
3
|
+
import { encodeEntityHash, parseEntityHash } from './entity_id.mjs'
|
|
4
|
+
|
|
4
5
|
/** @type {string} 逻辑实体 sentinel nodeHash(非物理节点绑定) */
|
|
5
6
|
export const LOGICAL_ENTITY_SENTINEL_NODE_HASH = '0'.repeat(64)
|
|
6
7
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { verifySignedPublicManifest } from '../files/public_manifest.mjs'
|
|
2
|
+
|
|
3
|
+
/** @type {Map<string, { expectedKey: string, timer: ReturnType<typeof setTimeout>, resolve: (v: object | null) => void }>} */
|
|
4
|
+
export const pendingManifestFetches = new Map()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_PENDING_MANIFEST_FETCHES = 512
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} ownerEntityHash owner
|
|
13
|
+
* @param {string} logicalPath 路径
|
|
14
|
+
* @returns {string} 期望键
|
|
15
|
+
*/
|
|
16
|
+
export function manifestFetchExpectedKey(ownerEntityHash, logicalPath) {
|
|
17
|
+
return `${String(ownerEntityHash || '').trim().toLowerCase()}\0${String(logicalPath || '').trim().replace(/^\/+/, '')}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} key requestId
|
|
22
|
+
* @param {string} expectedKey owner+path 复合键
|
|
23
|
+
* @param {number} timeoutMs 超时毫秒
|
|
24
|
+
* @returns {{ done: Promise<object | null>, cancel: () => void }} 等待 Promise 与取消
|
|
25
|
+
*/
|
|
26
|
+
export function registerManifestFetchWait(key, expectedKey, timeoutMs) {
|
|
27
|
+
if (!key || pendingManifestFetches.size >= MAX_PENDING_MANIFEST_FETCHES)
|
|
28
|
+
return {
|
|
29
|
+
done: Promise.resolve(null),
|
|
30
|
+
/**
|
|
31
|
+
*
|
|
32
|
+
*/
|
|
33
|
+
cancel: () => { },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @type {(value: object | null) => void} */
|
|
37
|
+
let settle
|
|
38
|
+
const done = new Promise(resolve => {
|
|
39
|
+
settle = resolve
|
|
40
|
+
})
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
pendingManifestFetches.delete(key)
|
|
43
|
+
settle(null)
|
|
44
|
+
}, timeoutMs)
|
|
45
|
+
|
|
46
|
+
pendingManifestFetches.set(key, {
|
|
47
|
+
expectedKey,
|
|
48
|
+
timer,
|
|
49
|
+
/**
|
|
50
|
+
* @param {object | null} value 验签后的 manifest,超时/取消为 null
|
|
51
|
+
*/
|
|
52
|
+
resolve: value => {
|
|
53
|
+
clearTimeout(timer)
|
|
54
|
+
pendingManifestFetches.delete(key)
|
|
55
|
+
settle(value)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
done,
|
|
61
|
+
/**
|
|
62
|
+
*
|
|
63
|
+
*/
|
|
64
|
+
cancel: () => {
|
|
65
|
+
clearTimeout(timer)
|
|
66
|
+
pendingManifestFetches.delete(key)
|
|
67
|
+
settle(null)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 处理 fed_manifest_data:验签通过后 resolve pending。
|
|
74
|
+
* @param {object} payload 入站载荷
|
|
75
|
+
* @returns {Promise<boolean>} 是否命中并完成等待
|
|
76
|
+
*/
|
|
77
|
+
export async function resolvePendingManifestFetch(payload) {
|
|
78
|
+
const requestId = String(payload?.requestId || '')
|
|
79
|
+
if (!requestId) return false
|
|
80
|
+
const entry = pendingManifestFetches.get(requestId)
|
|
81
|
+
if (!entry) return false
|
|
82
|
+
|
|
83
|
+
const verified = await verifySignedPublicManifest(payload?.manifest)
|
|
84
|
+
if (!verified) return false
|
|
85
|
+
const key = manifestFetchExpectedKey(verified.ownerEntityHash, verified.logicalPath)
|
|
86
|
+
if (key !== entry.expectedKey) return false
|
|
87
|
+
|
|
88
|
+
entry.resolve(verified)
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { isLogicalEntityHash } from '../logical_entity.mjs'
|
|
2
|
-
import { isWritableLocalEntity } from '../
|
|
1
|
+
import { isLogicalEntityHash } from '../core/logical_entity.mjs'
|
|
2
|
+
import { isWritableLocalEntity } from '../node/identity.mjs'
|
|
3
3
|
|
|
4
4
|
import { checkManifestAcl, resolveManifestAclType } from './manifest_acl_registry.mjs'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {string} replicaUsername 请求 replica
|
|
8
8
|
* @param {string} ownerEntityHash 文件 owner
|
|
9
|
-
* @param {import('
|
|
9
|
+
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
10
10
|
* @returns {Promise<boolean>} 是否允许读
|
|
11
11
|
*/
|
|
12
12
|
export async function canReadManifest(replicaUsername, ownerEntityHash, manifest) {
|
package/files/chunk_fetch.mjs
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
import { u8ToB64 } from '../core/bytes_codec.mjs'
|
|
4
|
-
import { FEDERATION_CHUNK_FETCH_FANOUT_K } from '../core/constants.mjs'
|
|
5
4
|
import {
|
|
6
5
|
MAX_PENDING_CHUNK_FETCHES,
|
|
7
6
|
pendingChunkFetches,
|
|
8
7
|
registerChunkFetchWait,
|
|
9
8
|
} from '../federation/chunk_fetch_pending.mjs'
|
|
10
|
-
import { loadNetwork } from '../node/network.mjs'
|
|
11
|
-
import { ensureLinkToNode, listLinks } from '../transport/link_registry.mjs'
|
|
12
|
-
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
13
9
|
|
|
14
10
|
import { verifiedChunkBytes } from './chunk_fetch_verify.mjs'
|
|
15
11
|
import { fetchFederationChunk, resolveNodeHash } from './chunk_provider_registry.mjs'
|
|
16
12
|
import { getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
13
|
+
import { fanoutFedFetch } from './fetch_fanout.mjs'
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
16
|
* @typedef {{
|
|
@@ -24,22 +21,6 @@ import { getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
|
24
21
|
* }} FetchChunkContext
|
|
25
22
|
*/
|
|
26
23
|
|
|
27
|
-
/**
|
|
28
|
-
* @returns {string[]} 全局 CAS miss 时应尝试拨号/发送的 nodeHash 列表
|
|
29
|
-
*/
|
|
30
|
-
function chunkFetchPeerTargets() {
|
|
31
|
-
/** @type {Set<string>} */
|
|
32
|
-
const targets = new Set()
|
|
33
|
-
for (const { nodeHash } of listLinks())
|
|
34
|
-
if (nodeHash) targets.add(String(nodeHash).toLowerCase())
|
|
35
|
-
const net = loadNetwork()
|
|
36
|
-
for (const nodeHash of [...net.trustedPeers || [], ...net.explorePeers || []])
|
|
37
|
-
if (nodeHash) targets.add(String(nodeHash).toLowerCase())
|
|
38
|
-
for (const hint of net.hints || [])
|
|
39
|
-
if (hint?.nodeHash) targets.add(String(hint.nodeHash).toLowerCase())
|
|
40
|
-
return [...targets]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
24
|
/**
|
|
44
25
|
* @param {FetchChunkContext} context 上下文
|
|
45
26
|
* @returns {Promise<Uint8Array | null>} 密文块
|
|
@@ -72,19 +53,7 @@ export async function fetchChunk(context) {
|
|
|
72
53
|
chunkHash: hash,
|
|
73
54
|
ownerEntityHash: context.ownerEntityHash,
|
|
74
55
|
}
|
|
75
|
-
|
|
76
|
-
const peerTargets = chunkFetchPeerTargets()
|
|
77
|
-
await Promise.all(peerTargets.map(nodeHash => ensureLinkToNode(nodeHash).catch(() => null)))
|
|
78
|
-
const tg = requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER)
|
|
79
|
-
// 已直连 / follow hint peer 可能不在 trust-graph top-K(非成员 emoji CAS / Social 预览路径)。
|
|
80
|
-
for (const nodeHash of peerTargets)
|
|
81
|
-
void tg.sendToNode(username, nodeHash, 'fed_chunk_get', payload, graph)
|
|
82
|
-
await tg.fanoutToTopNodes(
|
|
83
|
-
username,
|
|
84
|
-
'fed_chunk_get',
|
|
85
|
-
payload,
|
|
86
|
-
FEDERATION_CHUNK_FETCH_FANOUT_K,
|
|
87
|
-
)
|
|
56
|
+
await fanoutFedFetch(username, 'fed_chunk_get', payload)
|
|
88
57
|
const result = await done
|
|
89
58
|
const verified = verifiedChunkBytes(hash, result)
|
|
90
59
|
if (verified) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolvePendingChunkFetch } from '../federation/chunk_fetch_pending.mjs'
|
|
2
|
+
import { resolvePendingManifestFetch } from '../federation/manifest_fetch_pending.mjs'
|
|
2
3
|
|
|
3
4
|
import { handleIncomingChunkGet } from './chunk_fetch.mjs'
|
|
5
|
+
import { handleIncomingManifestGet } from './manifest_fetch.mjs'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* @param {string} username 副本用户名 用户名
|
|
@@ -22,7 +24,26 @@ export function handleFedChunkDataIngress(data) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
|
-
*
|
|
27
|
+
* @param {string} username 副本用户名
|
|
28
|
+
* @param {object} data 入站 fed_manifest_get
|
|
29
|
+
* @param {string} peerId 对端 id
|
|
30
|
+
* @param {(resp: object, peerId: string) => void} sendManifestData 发送 fed_manifest_data
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
export async function handleFedManifestGetIngress(username, data, peerId, sendManifestData) {
|
|
34
|
+
await handleIncomingManifestGet(username, data, sendManifestData, peerId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {object} data 入站 fed_manifest_data
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
export async function handleFedManifestDataIngress(data) {
|
|
42
|
+
await resolvePendingManifestFetch(data)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* node scope user-room wire:注册 fed_chunk_* + fed_manifest_*。
|
|
26
47
|
* @param {string} username 副本用户名 用户名
|
|
27
48
|
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
|
|
28
49
|
* @returns {void}
|
|
@@ -35,10 +56,19 @@ export function attachNodeScopeFedChunkResponder(username, wire) {
|
|
|
35
56
|
})
|
|
36
57
|
})
|
|
37
58
|
wire.on('fed_chunk_data', handleFedChunkDataIngress)
|
|
59
|
+
wire.on('fed_manifest_get', (data, peerId) => {
|
|
60
|
+
void handleFedManifestGetIngress(username, data, peerId, (resp, pid) => {
|
|
61
|
+
try { wire.send('fed_manifest_data', resp, pid) }
|
|
62
|
+
catch { /* disconnected */ }
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
wire.on('fed_manifest_data', data => {
|
|
66
|
+
void handleFedManifestDataIngress(data)
|
|
67
|
+
})
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
/**
|
|
41
|
-
* Trystero room:注册带 requestId 的
|
|
71
|
+
* Trystero room:注册带 requestId 的 fed_chunk_* + fed_manifest_*(TrustGraph 全局 miss)。
|
|
42
72
|
* @param {string} username 用户
|
|
43
73
|
* @param {object} room Trystero room
|
|
44
74
|
* @param {{ enqueue: (prio: number, fn: () => void) => void }} [fedOut] 出站队列
|
|
@@ -50,6 +80,8 @@ export function attachNodeScopeFedChunkResponder(username, wire) {
|
|
|
50
80
|
export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardGet, rtcLimits = {}, roomKey = '') {
|
|
51
81
|
const [sendChunkData, getChunkData] = room.makeAction('fed_chunk_data')
|
|
52
82
|
const [, getChunkGet] = room.makeAction('fed_chunk_get')
|
|
83
|
+
const [sendManifestData, getManifestData] = room.makeAction('fed_manifest_data')
|
|
84
|
+
const [, getManifestGet] = room.makeAction('fed_manifest_get')
|
|
53
85
|
|
|
54
86
|
getChunkGet((data, peerId) => {
|
|
55
87
|
if (guardGet && !guardGet(roomKey, 'fed_chunk_get', rtcLimits)) return
|
|
@@ -76,4 +108,30 @@ export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardG
|
|
|
76
108
|
if (!data || typeof data !== 'object' || !data.requestId) return
|
|
77
109
|
handleFedChunkDataIngress(data)
|
|
78
110
|
})
|
|
111
|
+
|
|
112
|
+
getManifestGet((data, peerId) => {
|
|
113
|
+
if (guardGet && !guardGet(roomKey, 'fed_manifest_get', rtcLimits)) return
|
|
114
|
+
void (async () => {
|
|
115
|
+
if (!data || typeof data !== 'object') return
|
|
116
|
+
if (!String(data.requestId || '')) return
|
|
117
|
+
await handleFedManifestGetIngress(username, data, peerId, (resp, pid) => {
|
|
118
|
+
/**
|
|
119
|
+
*
|
|
120
|
+
*/
|
|
121
|
+
const send = () => {
|
|
122
|
+
try { sendManifestData(resp, pid) }
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.warn('federation: trust-graph manifest response failed', error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (fedOut) fedOut.enqueue(6, send)
|
|
128
|
+
else send()
|
|
129
|
+
})
|
|
130
|
+
})().catch(error => console.warn('federation: trust-graph manifest handler failed', error))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
getManifestData(data => {
|
|
134
|
+
if (!data || typeof data !== 'object' || !data.requestId) return
|
|
135
|
+
void handleFedManifestDataIngress(data)
|
|
136
|
+
})
|
|
79
137
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { Readable } from 'node:stream'
|
|
3
3
|
|
|
4
|
-
import { FEDERATION_CHUNK_MAX_BYTES } from '
|
|
5
|
-
import {
|
|
6
|
-
import { encryptReadableToParts } from '../../files/assemble_stream.mjs'
|
|
7
|
-
import { fetchChunk } from '../../files/chunk_fetch.mjs'
|
|
8
|
-
import { getChunk, hasChunk, putChunk } from '../../files/chunk_store.mjs'
|
|
9
|
-
import { normalizeFileManifest, publicTransferKeyDescriptor } from '../../files/manifest.mjs'
|
|
10
|
-
import { assembleManifestPlaintext } from '../../files/transfer_key.mjs'
|
|
11
|
-
import { readDagManifestPlaintext, resolveTransferKeyDeps } from '../../files/transfer_key_registry.mjs'
|
|
12
|
-
import { getEntityStore } from '../../node/instance.mjs'
|
|
4
|
+
import { FEDERATION_CHUNK_MAX_BYTES } from '../core/constants.mjs'
|
|
5
|
+
import { getEntityStore } from '../node/instance.mjs'
|
|
13
6
|
|
|
7
|
+
import { buildFileManifestFromEnc, encryptPlaintextToParts, encryptPlaintextToMultiPartsAsync } from './assemble.mjs'
|
|
8
|
+
import { encryptReadableToParts } from './assemble_stream.mjs'
|
|
9
|
+
import { fetchChunk } from './chunk_fetch.mjs'
|
|
10
|
+
import { getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
11
|
+
import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
|
|
12
|
+
import { assembleManifestPlaintext } from './transfer_key.mjs'
|
|
13
|
+
import { readDagManifestPlaintext, resolveTransferKeyDeps } from './transfer_key_registry.mjs'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {string} ownerEntityHash 所有者
|
|
17
17
|
* @param {string} logicalPath 路径
|
|
18
|
-
* @returns {Promise<import('
|
|
18
|
+
* @returns {Promise<import('./manifest.mjs').FileManifest | null>} 归一化 manifest
|
|
19
19
|
*/
|
|
20
20
|
export async function loadFileManifest(ownerEntityHash, logicalPath) {
|
|
21
21
|
const manifest = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
|
|
@@ -23,7 +23,7 @@ export async function loadFileManifest(ownerEntityHash, logicalPath) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* @param {import('
|
|
26
|
+
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
27
27
|
* @returns {Promise<void>}
|
|
28
28
|
*/
|
|
29
29
|
export async function saveFileManifest(manifest) {
|
|
@@ -31,7 +31,7 @@ export async function saveFileManifest(manifest) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* @param {import('
|
|
34
|
+
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
35
35
|
* @param {Array<Buffer | Uint8Array>} partBytes 密文块
|
|
36
36
|
* @returns {Promise<void>}
|
|
37
37
|
*/
|
|
@@ -42,7 +42,7 @@ export async function storeManifestParts(manifest, partBytes) {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* @param {string} replicaUsername 副本用户名
|
|
45
|
-
* @param {import('
|
|
45
|
+
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
46
46
|
* @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
|
|
47
47
|
* @returns {Promise<Buffer | null>} 明文内容
|
|
48
48
|
*/
|
|
@@ -90,7 +90,7 @@ export async function readManifestPlaintext(replicaUsername, manifest, opts = {}
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* @param {string} replicaUsername 副本用户名
|
|
93
|
-
* @param {import('
|
|
93
|
+
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
94
94
|
* @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
|
|
95
95
|
* @returns {Promise<import('node:stream').Readable | null>} 明文流
|
|
96
96
|
*/
|
|
@@ -107,13 +107,12 @@ export async function readManifestPlaintextStream(replicaUsername, manifest, opt
|
|
|
107
107
|
* @param {Buffer | Uint8Array} params.plaintext 明文
|
|
108
108
|
* @param {string} [params.name] 文件名
|
|
109
109
|
* @param {string} [params.mimeType] MIME
|
|
110
|
-
* @param {import('
|
|
111
|
-
* @param {import('
|
|
110
|
+
* @param {import('./manifest.mjs').CeMode} [params.ceMode] 模式
|
|
111
|
+
* @param {import('./manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
|
|
112
112
|
* @param {object} [params.meta] meta
|
|
113
|
-
* @returns {Promise<import('
|
|
113
|
+
* @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
|
|
114
114
|
*/
|
|
115
115
|
export async function putFileManifest(params) {
|
|
116
|
-
const { encryptPlaintextToParts, encryptPlaintextToMultiPartsAsync } = await import('../../files/assemble.mjs')
|
|
117
116
|
const {
|
|
118
117
|
ownerEntityHash,
|
|
119
118
|
logicalPath,
|
|
@@ -152,10 +151,10 @@ export async function putFileManifest(params) {
|
|
|
152
151
|
* @param {number} params.plainSize 明文字节数
|
|
153
152
|
* @param {string} [params.name] 文件名
|
|
154
153
|
* @param {string} [params.mimeType] MIME
|
|
155
|
-
* @param {import('
|
|
156
|
-
* @param {import('
|
|
154
|
+
* @param {import('./manifest.mjs').CeMode} [params.ceMode] 模式
|
|
155
|
+
* @param {import('./manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
|
|
157
156
|
* @param {object} [params.meta] meta
|
|
158
|
-
* @returns {Promise<import('
|
|
157
|
+
* @returns {Promise<import('./manifest.mjs').FileManifest>} 写入后的 manifest
|
|
159
158
|
*/
|
|
160
159
|
export async function putFileManifestFromStream(params) {
|
|
161
160
|
const {
|
|
@@ -187,3 +186,22 @@ export async function putFileManifestFromStream(params) {
|
|
|
187
186
|
await saveFileManifest(manifest)
|
|
188
187
|
return manifest
|
|
189
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 读取实体公开文件:本地 miss 时经网络取回签名 manifest,chunk miss 走既有 fetchChunk。
|
|
192
|
+
* @param {string} replicaUsername 副本用户名
|
|
193
|
+
* @param {string} entityHash owner entityHash
|
|
194
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
195
|
+
* @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
|
|
196
|
+
* @returns {Promise<Buffer | null>} 明文或 null
|
|
197
|
+
*/
|
|
198
|
+
export async function readPublicFile(replicaUsername, entityHash, logicalPath, opts = {}) {
|
|
199
|
+
const { fetchPublicManifest } = await import('./manifest_fetch.mjs')
|
|
200
|
+
const manifest = await fetchPublicManifest({
|
|
201
|
+
username: opts.username || replicaUsername,
|
|
202
|
+
ownerEntityHash: entityHash,
|
|
203
|
+
logicalPath,
|
|
204
|
+
})
|
|
205
|
+
if (!manifest) return null
|
|
206
|
+
return readManifestPlaintext(replicaUsername, manifest, opts)
|
|
207
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { isEntityHash128 } from '
|
|
2
|
-
import { assertSafeEvfsLogicalPath } from '
|
|
1
|
+
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
2
|
+
import { assertSafeEvfsLogicalPath } from '../core/evfs_logical_path.mjs'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
*/
|
|
4
|
+
/** @type {string} evfs URI scheme */
|
|
7
5
|
export const EVFS_SCHEME = 'evfs:'
|
|
8
6
|
|
|
9
7
|
/**
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FEDERATION_CHUNK_FETCH_FANOUT_K } from '../core/constants.mjs'
|
|
2
|
+
import { loadNetwork } from '../node/network.mjs'
|
|
3
|
+
import { ensureLinkToNode, listLinks } from '../transport/link_registry.mjs'
|
|
4
|
+
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {string[]} 全局 miss 时应尝试拨号/发送的 nodeHash 列表
|
|
8
|
+
*/
|
|
9
|
+
function fetchPeerTargets() {
|
|
10
|
+
/** @type {Set<string>} */
|
|
11
|
+
const targets = new Set()
|
|
12
|
+
for (const { nodeHash } of listLinks())
|
|
13
|
+
if (nodeHash) targets.add(String(nodeHash).toLowerCase())
|
|
14
|
+
const net = loadNetwork()
|
|
15
|
+
for (const nodeHash of [...net.trustedPeers || [], ...net.explorePeers || []])
|
|
16
|
+
if (nodeHash) targets.add(String(nodeHash).toLowerCase())
|
|
17
|
+
for (const hint of net.hints || [])
|
|
18
|
+
if (hint?.nodeHash) targets.add(String(hint.nodeHash).toLowerCase())
|
|
19
|
+
return [...targets]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 全局 miss 请求扇出:先向已知 peer 定向发送,再 trust-graph top-K fanout。
|
|
24
|
+
* 已直连 / follow hint peer 可能不在 trust-graph top-K(非成员 emoji CAS / Social 预览路径),故先定向发送。
|
|
25
|
+
* @param {string} username 用户
|
|
26
|
+
* @param {string} action wire action 名
|
|
27
|
+
* @param {object} payload 请求载荷
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
export async function fanoutFedFetch(username, action, payload) {
|
|
31
|
+
const tg = requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER)
|
|
32
|
+
const graph = await tg.buildMergedGraph(username)
|
|
33
|
+
const peerTargets = fetchPeerTargets()
|
|
34
|
+
await Promise.all(peerTargets.map(nodeHash => ensureLinkToNode(nodeHash).catch(() => null)))
|
|
35
|
+
for (const nodeHash of peerTargets)
|
|
36
|
+
void tg.sendToNode(username, nodeHash, action, payload, graph)
|
|
37
|
+
await tg.fanoutToTopNodes(username, action, payload, FEDERATION_CHUNK_FETCH_FANOUT_K)
|
|
38
|
+
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
/** @type {Map<string, { ownerId: string, handler: (context: ManifestAclContext, logicalPath?: string) => Promise<boolean> }>} */
|
|
6
6
|
const handlersByType = new Map()
|
|
7
7
|
|
|
8
|
-
/** @type {Map<string, { ownerId: string, match: (manifest: import('
|
|
8
|
+
/** @type {Map<string, { ownerId: string, match: (manifest: import('./manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null }>} */
|
|
9
9
|
const matchersByOwner = new Map()
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {{
|
|
13
13
|
* replicaUsername: string,
|
|
14
14
|
* ownerEntityHash: string,
|
|
15
|
-
* manifest: import('
|
|
15
|
+
* manifest: import('./manifest.mjs').FileManifest,
|
|
16
16
|
* }} ManifestAclContext
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -46,7 +46,7 @@ export function unregisterManifestAcl(type, ownerId) {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* @param {string} ownerId 注册方
|
|
49
|
-
* @param {(manifest: import('
|
|
49
|
+
* @param {(manifest: import('./manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null} match ACL 类型匹配
|
|
50
50
|
* @returns {void}
|
|
51
51
|
*/
|
|
52
52
|
export function registerManifestAclMatcher(ownerId, match) {
|
|
@@ -68,7 +68,7 @@ export function clearManifestAclRegistry() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* @param {import('
|
|
71
|
+
* @param {import('./manifest.mjs').FileManifest | null | undefined} manifest manifest
|
|
72
72
|
* @param {string} ownerEntityHash 所有者
|
|
73
73
|
* @returns {string | null} ACL 类型
|
|
74
74
|
*/
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
manifestFetchExpectedKey,
|
|
5
|
+
MAX_PENDING_MANIFEST_FETCHES,
|
|
6
|
+
pendingManifestFetches,
|
|
7
|
+
registerManifestFetchWait,
|
|
8
|
+
} from '../federation/manifest_fetch_pending.mjs'
|
|
9
|
+
import { isWritableLocalEntity } from '../node/identity.mjs'
|
|
10
|
+
import { getEntityStore } from '../node/instance.mjs'
|
|
11
|
+
|
|
12
|
+
import { resolveNodeHash } from './chunk_provider_registry.mjs'
|
|
13
|
+
import { loadFileManifest, saveFileManifest } from './evfs.mjs'
|
|
14
|
+
import { fanoutFedFetch } from './fetch_fanout.mjs'
|
|
15
|
+
import { normalizeFileManifest } from './manifest.mjs'
|
|
16
|
+
import { shouldPreferIncomingPublicManifest } from './public_manifest.mjs'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ username: string, ownerEntityHash: string, logicalPath: string }} context 上下文
|
|
20
|
+
* @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签后的公开清单
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchPublicManifest(context) {
|
|
23
|
+
const ownerEntityHash = String(context.ownerEntityHash || '').trim().toLowerCase()
|
|
24
|
+
const logicalPath = String(context.logicalPath || '').trim().replace(/^\/+/, '')
|
|
25
|
+
const { username } = context
|
|
26
|
+
if (!ownerEntityHash || !logicalPath || !username) return null
|
|
27
|
+
|
|
28
|
+
const local = await loadFileManifest(ownerEntityHash, logicalPath)
|
|
29
|
+
if (local?.transferKeyDescriptor?.type === 'public' && local?.meta?.publicSig)
|
|
30
|
+
return local
|
|
31
|
+
|
|
32
|
+
if (pendingManifestFetches.size >= MAX_PENDING_MANIFEST_FETCHES) return null
|
|
33
|
+
|
|
34
|
+
const requestId = randomUUID()
|
|
35
|
+
const { done } = registerManifestFetchWait(
|
|
36
|
+
requestId,
|
|
37
|
+
manifestFetchExpectedKey(ownerEntityHash, logicalPath),
|
|
38
|
+
8000,
|
|
39
|
+
)
|
|
40
|
+
const { nodeHash } = await resolveNodeHash(username)
|
|
41
|
+
const payload = {
|
|
42
|
+
requestId,
|
|
43
|
+
nodeHash,
|
|
44
|
+
ownerEntityHash,
|
|
45
|
+
logicalPath,
|
|
46
|
+
}
|
|
47
|
+
await fanoutFedFetch(username, 'fed_manifest_get', payload)
|
|
48
|
+
const result = await done
|
|
49
|
+
if (!result) return null
|
|
50
|
+
|
|
51
|
+
await maybeCacheIncomingPublicManifest(ownerEntityHash, logicalPath, result)
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} ownerEntityHash owner
|
|
57
|
+
* @param {string} logicalPath 路径
|
|
58
|
+
* @param {import('./manifest.mjs').FileManifest} incoming 已验签入站清单
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
*/
|
|
61
|
+
async function maybeCacheIncomingPublicManifest(ownerEntityHash, logicalPath, incoming) {
|
|
62
|
+
if (isWritableLocalEntity(ownerEntityHash)) return
|
|
63
|
+
const store = getEntityStore()
|
|
64
|
+
const existing = await store.readManifest(ownerEntityHash, logicalPath)
|
|
65
|
+
if (existing && !shouldPreferIncomingPublicManifest(existing, incoming)) return
|
|
66
|
+
await saveFileManifest(incoming)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 若本机有已签名公开 manifest 则响应 fed_manifest_get。
|
|
71
|
+
* @param {string} username 用户
|
|
72
|
+
* @param {object} payload 请求
|
|
73
|
+
* @param {(response: object, peerId: string) => void} sendResponse 发送
|
|
74
|
+
* @param {string} peerId 对端
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
export async function handleIncomingManifestGet(username, payload, sendResponse, peerId) {
|
|
78
|
+
void username
|
|
79
|
+
const ownerEntityHash = String(payload?.ownerEntityHash || '').trim().toLowerCase()
|
|
80
|
+
const logicalPath = String(payload?.logicalPath || '').trim().replace(/^\/+/, '')
|
|
81
|
+
const requestId = String(payload?.requestId || '')
|
|
82
|
+
if (!ownerEntityHash || !logicalPath || !requestId) return
|
|
83
|
+
|
|
84
|
+
const raw = await getEntityStore().readManifest(ownerEntityHash, logicalPath)
|
|
85
|
+
const manifest = normalizeFileManifest(raw)
|
|
86
|
+
if (!manifest) return
|
|
87
|
+
if (manifest.transferKeyDescriptor.type !== 'public') return
|
|
88
|
+
if (!raw?.meta?.publicSig) return
|
|
89
|
+
|
|
90
|
+
sendResponse({
|
|
91
|
+
requestId,
|
|
92
|
+
manifest: {
|
|
93
|
+
...manifest,
|
|
94
|
+
meta: { publicSig: raw.meta.publicSig },
|
|
95
|
+
},
|
|
96
|
+
}, peerId)
|
|
97
|
+
}
|