@steve02081504/fount-p2p 0.0.1 → 0.0.3

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
@@ -44,7 +44,8 @@ Root contains only the facade and package metadata; all modules live in layered
44
44
  ## Tests
45
45
 
46
46
  ```bash
47
- npm test # pure + integration (--test-force-exit: node-datachannel leaves native handles)
47
+ npm test # package pure + integration (Node)
48
+ npm run test:fount # cross-repo bridge (Deno; requires fount on PATH)
48
49
  npm run test:live # RTC / link smoke (requires node-datachannel)
49
50
  npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
50
51
  ```
@@ -34,18 +34,18 @@ export function hashFromPubKeyHex(pubKeyHex) {
34
34
  /**
35
35
  * @param {string} nodeHash 成员所属节点 hash
36
36
  * @param {string} recoveryPubKeyHex 32 字节 recovery 公钥 hex(稳定身份锚)
37
- * @returns {string} 用户 entityHash
37
+ * @returns {string} entityHash
38
38
  */
39
- export function userEntityHashFromRecoveryPubKeyHex(nodeHash, recoveryPubKeyHex) {
39
+ export function entityHashFromRecoveryPubKeyHex(nodeHash, recoveryPubKeyHex) {
40
40
  return encodeEntityHash(nodeHash, hashFromPubKeyHex(recoveryPubKeyHex))
41
41
  }
42
42
 
43
43
  /**
44
44
  * @param {string} nodeHash 成员所属节点 hash
45
45
  * @param {string} subjectHash 成员签名 pubKeyHash(DAG sender)
46
- * @returns {string} 用户 entityHash
46
+ * @returns {string} entityHash
47
47
  */
48
- export function userEntityHashFromSubjectHash(nodeHash, subjectHash) {
48
+ export function entityHashFromSubjectHash(nodeHash, subjectHash) {
49
49
  const node = normalizeHex64(nodeHash)
50
50
  const subject = normalizeHex64(subjectHash)
51
51
  if (!isHex64(node) || !isHex64(subject)) throw new Error('invalid subject hash')
package/deno.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "nodeModulesDir": "none",
3
+ "lock": false
4
+ }
package/discovery/bt.mjs CHANGED
@@ -53,7 +53,7 @@ export async function waitPoweredOn(runtime, timeout) {
53
53
 
54
54
  /**
55
55
  * 探测本机 noble 运行时是否具备 BT 扫描所需 API。
56
- * @returns {Promise<boolean>}
56
+ * @returns {Promise<boolean>} noble 可加载且具备 startScanningAsync 与 waitForPoweredOn(Async) 时为 true
57
57
  */
58
58
  export async function canUseBluetoothDiscovery() {
59
59
  try {
@@ -187,3 +187,22 @@ export async function putFileManifestFromStream(params) {
187
187
  await saveFileManifest(manifest)
188
188
  return manifest
189
189
  }
190
+
191
+ /**
192
+ * 读取实体公开文件:本地 miss 时经网络取回签名 manifest,chunk miss 走既有 fetchChunk。
193
+ * @param {string} replicaUsername 副本用户名
194
+ * @param {string} entityHash owner entityHash
195
+ * @param {string} logicalPath EVFS 逻辑路径
196
+ * @param {{ username?: string, fetchChunk?: Function }} [opts] miss 拉取
197
+ * @returns {Promise<Buffer | null>} 明文或 null
198
+ */
199
+ export async function readPublicFile(replicaUsername, entityHash, logicalPath, opts = {}) {
200
+ const { fetchPublicManifest } = await import('../../files/manifest_fetch.mjs')
201
+ const manifest = await fetchPublicManifest({
202
+ username: opts.username || replicaUsername,
203
+ ownerEntityHash: entityHash,
204
+ logicalPath,
205
+ })
206
+ if (!manifest) return null
207
+ return readManifestPlaintext(replicaUsername, manifest, opts)
208
+ }
@@ -1,12 +1,12 @@
1
1
  import { parseEntityHash } from '../core/entity_id.mjs'
2
- import { operatorEntityHashFromKeys, getNodeHash } from '../node/identity.mjs'
2
+ import { entityHashFromKeys, getNodeHash } from '../node/identity.mjs'
3
3
 
4
4
  /**
5
5
  * @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
6
- * @returns {string | null} 本节点操作者 entityHash
6
+ * @returns {string | null} 本节点 entityHash
7
7
  */
8
- export function resolveLocalOperatorEntityHash(recoveryPubKeyHex) {
9
- return operatorEntityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
8
+ export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
9
+ return entityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
10
10
  }
11
11
 
12
12
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Operator 密钥历史链:entityHash 锚定 recovery 公钥,活跃钥可轮换。
2
+ * Entity 密钥历史链:entityHash 锚定 recovery 公钥,活跃钥可轮换。
3
3
  */
4
4
  import { Buffer } from 'node:buffer'
5
5
 
@@ -10,7 +10,7 @@ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
10
10
  /**
11
11
  *
12
12
  */
13
- export const OPERATOR_KEY_REVOKE_DOMAIN = 'fount-operator-key-revoke'
13
+ export const ENTITY_KEY_REVOKE_DOMAIN = 'fount-entity-key-revoke'
14
14
 
15
15
  /**
16
16
  * @typedef {{
@@ -19,7 +19,7 @@ export const OPERATOR_KEY_REVOKE_DOMAIN = 'fount-operator-key-revoke'
19
19
  * attestedBy: 'recovery' | 'active' | 'revoked',
20
20
  * validFrom?: number,
21
21
  * revokedGenerations?: number[],
22
- * }} OperatorKeyHistoryEntry
22
+ * }} EntityKeyHistoryEntry
23
23
  */
24
24
 
25
25
  /**
@@ -42,7 +42,7 @@ export function activeSenderHashFromPubKeyHex(activePubKeyHex) {
42
42
  * @param {string} recoveryPubKeyHex recovery 公钥
43
43
  * @param {string} activePubKeyHex 初始活跃公钥
44
44
  * @param {number} [validFrom] 生效时间
45
- * @returns {OperatorKeyHistoryEntry[]} 创世链
45
+ * @returns {EntityKeyHistoryEntry[]} 创世链
46
46
  */
47
47
  export function createGenesisKeyHistory(recoveryPubKeyHex, activePubKeyHex, validFrom = Date.now()) {
48
48
  return [{
@@ -54,7 +54,7 @@ export function createGenesisKeyHistory(recoveryPubKeyHex, activePubKeyHex, vali
54
54
  }
55
55
 
56
56
  /**
57
- * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
57
+ * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
58
58
  * @param {number} generation 代际
59
59
  * @returns {string | null} 活跃公钥 hex
60
60
  */
@@ -64,7 +64,7 @@ export function resolveActiveKeyAtGeneration(keyHistory, generation) {
64
64
  }
65
65
 
66
66
  /**
67
- * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
67
+ * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
68
68
  * @returns {number} 最新代际,无则 -1
69
69
  */
70
70
  export function resolveLatestActiveGeneration(keyHistory) {
@@ -73,7 +73,7 @@ export function resolveLatestActiveGeneration(keyHistory) {
73
73
  }
74
74
 
75
75
  /**
76
- * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
76
+ * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
77
77
  * @param {number} generation 代际
78
78
  * @returns {boolean} 是否已吊销
79
79
  */
@@ -84,7 +84,7 @@ export function isActiveGenerationRevoked(keyHistory, generation) {
84
84
  }
85
85
 
86
86
  /**
87
- * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
87
+ * @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
88
88
  * @param {string} recoveryPubKeyHex recovery 公钥
89
89
  * @param {string} senderPubKeyHash 事件 sender(64 hex pubKeyHash)
90
90
  * @returns {boolean} sender 是否为未吊销的活跃钥
@@ -112,18 +112,18 @@ export function isRecoverySender(recoveryPubKeyHex, senderPubKeyHash) {
112
112
 
113
113
  /**
114
114
  * @param {object} state 物化状态
115
- * @param {object} event operator_key_rotate 事件
115
+ * @param {object} event entity_key_rotate 事件
116
116
  * @returns {object} 更新后状态
117
117
  */
118
- export function reduceOperatorKeyRotate(state, event) {
118
+ export function reduceEntityKeyRotate(state, event) {
119
119
  const generation = Number(event.content?.generation)
120
120
  const activePubKeyHex = normalizeHex64(event.content?.activePubKeyHex || '')
121
121
  if (!Number.isFinite(generation) || generation < 0 || !isHex64(activePubKeyHex))
122
122
  return state
123
- state.operatorKeyHistory = state.operatorKeyHistory || []
124
- if (state.operatorKeyHistory.some(row => row.generation === generation))
123
+ state.entityKeyHistory = state.entityKeyHistory || []
124
+ if (state.entityKeyHistory.some(row => row.generation === generation))
125
125
  return state
126
- state.operatorKeyHistory.push({
126
+ state.entityKeyHistory.push({
127
127
  generation,
128
128
  activePubKeyHex,
129
129
  attestedBy: generation === 0 ? 'recovery' : 'active',
@@ -134,10 +134,10 @@ export function reduceOperatorKeyRotate(state, event) {
134
134
 
135
135
  /**
136
136
  * @param {object} state 物化状态
137
- * @param {object} event operator_key_revoke 事件
137
+ * @param {object} event entity_key_revoke 事件
138
138
  * @returns {object} 更新后状态
139
139
  */
140
- export function reduceOperatorKeyRevoke(state, event) {
140
+ export function reduceEntityKeyRevoke(state, event) {
141
141
  const newGeneration = Number(event.content?.newGeneration)
142
142
  const activePubKeyHex = normalizeHex64(event.content?.activePubKeyHex || '')
143
143
  const revokeGenerations = Array.isArray(event.content?.revokeGenerations)
@@ -145,17 +145,17 @@ export function reduceOperatorKeyRevoke(state, event) {
145
145
  : []
146
146
  if (!Number.isFinite(newGeneration) || newGeneration < 0 || !isHex64(activePubKeyHex))
147
147
  return state
148
- state.operatorKeyHistory = state.operatorKeyHistory || []
148
+ state.entityKeyHistory = state.entityKeyHistory || []
149
149
  for (const gen of revokeGenerations) {
150
- const entry = state.operatorKeyHistory.find(row => row.generation === gen)
150
+ const entry = state.entityKeyHistory.find(row => row.generation === gen)
151
151
  if (entry) {
152
152
  entry.revokedGenerations = entry.revokedGenerations || []
153
153
  if (!entry.revokedGenerations.includes(gen))
154
154
  entry.revokedGenerations.push(gen)
155
155
  }
156
156
  }
157
- if (!state.operatorKeyHistory.some(row => row.generation === newGeneration))
158
- state.operatorKeyHistory.push({
157
+ if (!state.entityKeyHistory.some(row => row.generation === newGeneration))
158
+ state.entityKeyHistory.push({
159
159
  generation: newGeneration,
160
160
  activePubKeyHex,
161
161
  attestedBy: 'recovery',
@@ -166,35 +166,34 @@ export function reduceOperatorKeyRevoke(state, event) {
166
166
  }
167
167
  /**
168
168
  * @param {object[]} events 时间线事件(拓扑序)
169
- * @returns {{ recoveryPubKeyHex: string | null, operatorKeyHistory: OperatorKeyHistoryEntry[] }} 折叠密钥链
169
+ * @returns {{ recoveryPubKeyHex: string | null, entityKeyHistory: EntityKeyHistoryEntry[] }} 折叠密钥链
170
170
  */
171
- export function foldOperatorKeyHistoryFromEvents(events) {
172
- /** @type {OperatorKeyHistoryEntry[]} */
173
- let operatorKeyHistory = []
171
+ export function foldEntityKeyHistoryFromEvents(events) {
172
+ /** @type {EntityKeyHistoryEntry[]} */
173
+ let entityKeyHistory = []
174
174
  for (const event of events || []) {
175
- if (event.type === 'operator_key_rotate') {
176
- const state = reduceOperatorKeyRotate({ operatorKeyHistory }, event)
177
- operatorKeyHistory = state.operatorKeyHistory
175
+ if (event.type === 'entity_key_rotate') {
176
+ const state = reduceEntityKeyRotate({ entityKeyHistory }, event)
177
+ entityKeyHistory = state.entityKeyHistory
178
178
  }
179
- if (event.type === 'operator_key_revoke') {
180
- const state = reduceOperatorKeyRevoke({ operatorKeyHistory }, event)
181
- operatorKeyHistory = state.operatorKeyHistory
179
+ if (event.type === 'entity_key_revoke') {
180
+ const state = reduceEntityKeyRevoke({ entityKeyHistory }, event)
181
+ entityKeyHistory = state.entityKeyHistory
182
182
  }
183
183
  }
184
- return { recoveryPubKeyHex: null, operatorKeyHistory }
184
+ return { recoveryPubKeyHex: null, entityKeyHistory }
185
185
  }
186
186
 
187
187
  /**
188
188
  * @param {object} revokeBody 吊销正文
189
189
  * @returns {Buffer} 固定域签名消息
190
190
  */
191
- export function operatorKeyRevokeSignBytes(revokeBody) {
191
+ export function entityKeyRevokeSignBytes(revokeBody) {
192
192
  const body = {
193
193
  revokeGenerations: (revokeBody.revokeGenerations || []).map(g => Number(g)),
194
194
  newGeneration: Number(revokeBody.newGeneration),
195
195
  activePubKeyHex: normalizeHex64(revokeBody.activePubKeyHex || ''),
196
196
  entityHash: String(revokeBody.entityHash || '').trim().toLowerCase(),
197
197
  }
198
- return Buffer.from(`${OPERATOR_KEY_REVOKE_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
198
+ return Buffer.from(`${ENTITY_KEY_REVOKE_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
199
199
  }
200
-
@@ -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,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,29 +21,13 @@ 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>} 密文块
46
27
  */
47
28
  export async function fetchChunk(context) {
48
29
  const hash = String(context.ciphertextHash || '').trim().toLowerCase()
49
- const username = context.username
30
+ const { username } = context
50
31
  if (!hash || !username) return null
51
32
 
52
33
  if (await hasChunk(hash))
@@ -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
  }
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import { loadFileManifest, saveFileManifest } from '../entity/files/evfs.mjs'
4
+ import { isWritableLocalEntity } from '../entity/replica.mjs'
5
+ import {
6
+ manifestFetchExpectedKey,
7
+ MAX_PENDING_MANIFEST_FETCHES,
8
+ pendingManifestFetches,
9
+ registerManifestFetchWait,
10
+ } from '../federation/manifest_fetch_pending.mjs'
11
+ import { getEntityStore } from '../node/instance.mjs'
12
+
13
+ import { resolveNodeHash } from './chunk_provider_registry.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
+ }