@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 +2 -1
- package/core/entity_id.mjs +4 -4
- package/deno.json +4 -0
- package/discovery/bt.mjs +1 -1
- package/entity/files/evfs.mjs +19 -0
- package/entity/replica.mjs +4 -4
- package/federation/{operator_key_chain.mjs → entity_key_chain.mjs} +32 -33
- package/federation/manifest_fetch_pending.mjs +90 -0
- package/files/chunk_fetch.mjs +3 -34
- package/files/chunk_responder.mjs +60 -2
- package/files/fetch_fanout.mjs +38 -0
- package/files/manifest_fetch.mjs +97 -0
- package/files/public_manifest.mjs +153 -0
- package/files/transfer_key.mjs +2 -4
- package/governance/join_pow.mjs +1 -1
- package/link/channel_mux.mjs +3 -1
- package/link/link.mjs +1 -1
- package/link/sdp_fingerprint.mjs +1 -1
- package/mailbox/store.mjs +1 -1
- package/node/denylist.mjs +2 -2
- package/node/identity.mjs +5 -5
- package/node/personal_block.mjs +1 -1
- package/overlay/index.mjs +1 -1
- package/package.json +2 -1
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/stale_peer_log.mjs +0 -3
- package/wire/part_invoke.mjs +1 -1
- package/wire/rate_bucket.mjs +1 -1
- package/entity/files/url.mjs +0 -19
- package/entity/localized_core.mjs +0 -141
- package/entity/presentation_registry.mjs +0 -44
- package/entity/profile.mjs +0 -256
- package/utils/debug_log.mjs +0 -16
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 (
|
|
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
|
```
|
package/core/entity_id.mjs
CHANGED
|
@@ -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}
|
|
37
|
+
* @returns {string} entityHash
|
|
38
38
|
*/
|
|
39
|
-
export function
|
|
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}
|
|
46
|
+
* @returns {string} entityHash
|
|
47
47
|
*/
|
|
48
|
-
export function
|
|
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
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 {
|
package/entity/files/evfs.mjs
CHANGED
|
@@ -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
|
+
}
|
package/entity/replica.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
2
|
-
import {
|
|
2
|
+
import { entityHashFromKeys, getNodeHash } from '../node/identity.mjs'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
|
|
6
|
-
* @returns {string | null}
|
|
6
|
+
* @returns {string | null} 本节点 entityHash
|
|
7
7
|
*/
|
|
8
|
-
export function
|
|
9
|
-
return
|
|
8
|
+
export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
|
|
9
|
+
return entityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
-
* }}
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
115
|
+
* @param {object} event entity_key_rotate 事件
|
|
116
116
|
* @returns {object} 更新后状态
|
|
117
117
|
*/
|
|
118
|
-
export function
|
|
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.
|
|
124
|
-
if (state.
|
|
123
|
+
state.entityKeyHistory = state.entityKeyHistory || []
|
|
124
|
+
if (state.entityKeyHistory.some(row => row.generation === generation))
|
|
125
125
|
return state
|
|
126
|
-
state.
|
|
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
|
|
137
|
+
* @param {object} event entity_key_revoke 事件
|
|
138
138
|
* @returns {object} 更新后状态
|
|
139
139
|
*/
|
|
140
|
-
export function
|
|
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.
|
|
148
|
+
state.entityKeyHistory = state.entityKeyHistory || []
|
|
149
149
|
for (const gen of revokeGenerations) {
|
|
150
|
-
const entry = state.
|
|
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.
|
|
158
|
-
state.
|
|
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,
|
|
169
|
+
* @returns {{ recoveryPubKeyHex: string | null, entityKeyHistory: EntityKeyHistoryEntry[] }} 折叠密钥链
|
|
170
170
|
*/
|
|
171
|
-
export function
|
|
172
|
-
/** @type {
|
|
173
|
-
let
|
|
171
|
+
export function foldEntityKeyHistoryFromEvents(events) {
|
|
172
|
+
/** @type {EntityKeyHistoryEntry[]} */
|
|
173
|
+
let entityKeyHistory = []
|
|
174
174
|
for (const event of events || []) {
|
|
175
|
-
if (event.type === '
|
|
176
|
-
const state =
|
|
177
|
-
|
|
175
|
+
if (event.type === 'entity_key_rotate') {
|
|
176
|
+
const state = reduceEntityKeyRotate({ entityKeyHistory }, event)
|
|
177
|
+
entityKeyHistory = state.entityKeyHistory
|
|
178
178
|
}
|
|
179
|
-
if (event.type === '
|
|
180
|
-
const state =
|
|
181
|
-
|
|
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,
|
|
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
|
|
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(`${
|
|
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
|
+
}
|
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,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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|