@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 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/`, `entity/` | Federation, store-and-forward, DAG, EVFS |
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 '../replica.mjs'
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('../../files/manifest.mjs').FileManifest} manifest 清单
9
+ * @param {import('./manifest.mjs').FileManifest} manifest 清单
10
10
  * @returns {Promise<boolean>} 是否允许读
11
11
  */
12
12
  export async function canReadManifest(replicaUsername, ownerEntityHash, manifest) {
@@ -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
- const graph = await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).buildMergedGraph(username)
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
- * node scope user-room wire:注册 fed_chunk_get / fed_chunk_data。
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 的 fed_chunk_get / fed_chunk_data(TrustGraph 全局 miss)。
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 '../../core/constants.mjs'
5
- import { buildFileManifestFromEnc } from '../../files/assemble.mjs'
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('../../files/manifest.mjs').FileManifest | null>} 归一化 manifest
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('../../files/manifest.mjs').FileManifest} manifest 清单
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('../../files/manifest.mjs').FileManifest} manifest 清单
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('../../files/manifest.mjs').FileManifest} manifest 清单
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('../../files/manifest.mjs').FileManifest} manifest 清单
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('../../files/manifest.mjs').CeMode} [params.ceMode] 模式
111
- * @param {import('../../files/manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
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('../../files/manifest.mjs').FileManifest>} 写入后的 manifest
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('../../files/manifest.mjs').CeMode} [params.ceMode] 模式
156
- * @param {import('../../files/manifest.mjs').TransferKeyDescriptor} [params.transferKeyDescriptor] 传递密钥
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('../../files/manifest.mjs').FileManifest>} 写入后的 manifest
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 '../../core/entity_id.mjs'
2
- import { assertSafeEvfsLogicalPath } from '../../core/evfs_logical_path.mjs'
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('../../files/manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null }>} */
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('../../files/manifest.mjs').FileManifest,
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('../../files/manifest.mjs').FileManifest | null | undefined, ownerEntityHash: string) => string | null} match ACL 类型匹配
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('../../files/manifest.mjs').FileManifest | null | undefined} manifest manifest
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
+ }