@steve02081504/fount-p2p 0.0.26 → 0.0.28
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 +3 -2
- package/core/bytes_codec.mjs +5 -6
- package/core/composite_key.mjs +0 -79
- package/core/entity_id_parse.mjs +2 -3
- package/core/evfs_logical_path.mjs +2 -3
- package/core/hexIds.mjs +5 -5
- package/core/logical_entity.mjs +3 -4
- package/crypto/channel.mjs +2 -25
- package/crypto/checkpoint_sign.mjs +5 -4
- package/crypto/crypto.mjs +0 -8
- package/crypto/key.mjs +9 -10
- package/dag/canonicalize_row.mjs +1 -1
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +7 -12
- package/discovery/bt/index.mjs +6 -11
- package/discovery/bt/peer_hints.mjs +2 -3
- package/discovery/bt/runtime.mjs +1 -1
- package/discovery/index.mjs +14 -14
- package/discovery/lan.mjs +3 -3
- package/discovery/lan_interfaces.mjs +2 -2
- package/discovery/lan_peer_hints.mjs +1 -1
- package/discovery/nostr.mjs +19 -24
- package/federation/entity_key_chain.mjs +5 -24
- package/federation/message_rate_limit.mjs +3 -2
- package/federation/part_query/runtime.mjs +9 -20
- package/files/assemble.mjs +3 -3
- package/files/assemble_stream.mjs +1 -1
- package/files/chunk/fetch.mjs +1 -2
- package/files/chunk/responder.mjs +4 -41
- package/files/chunk/store.mjs +2 -3
- package/files/chunk/verify.mjs +2 -3
- package/files/evfs.mjs +1 -1
- package/files/evfs_ref.mjs +3 -4
- package/files/fetch_fanout.mjs +3 -3
- package/files/manifest/fetch.mjs +2 -3
- package/files/manifest/normalize.mjs +9 -9
- package/files/manifest/pending.mjs +1 -4
- package/files/manifest/public.mjs +8 -8
- package/files/transfer_key.mjs +3 -3
- package/files/transfer_key_registry.mjs +6 -6
- package/governance/branch.mjs +2 -1
- package/governance/join_pow.mjs +7 -5
- package/governance/owner_succession_ballot.mjs +6 -6
- package/index.mjs +8 -2
- package/infra/default_node_dir.mjs +2 -2
- package/infra/priority.mjs +2 -2
- package/link/frame.mjs +2 -3
- package/link/handshake.mjs +6 -6
- package/link/pipe.mjs +69 -5
- package/link/providers/ble_gatt.mjs +1 -9
- package/link/providers/index.mjs +2 -2
- package/link/providers/nostr.mjs +1 -3
- package/link/providers/webrtc.mjs +2 -2
- package/link/rtc/ice_local_hostname.mjs +6 -5
- package/link/rtc/w3c_bridge.mjs +1 -1
- package/link/sdp_fingerprint.mjs +1 -1
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +4 -4
- package/mailbox/parse.mjs +1 -1
- package/mailbox/prune.mjs +2 -2
- package/mailbox/rate.mjs +3 -10
- package/mailbox/store.mjs +7 -7
- package/node/denylist.mjs +38 -38
- package/node/entity_store.mjs +5 -5
- package/node/identity.mjs +7 -8
- package/node/instance.mjs +1 -1
- package/node/log.mjs +1 -1
- package/node/network.mjs +14 -16
- package/node/personal_block.mjs +22 -20
- package/node/reputation_store.mjs +11 -19
- package/node/reputation_sync.mjs +0 -7
- package/node/signaling_config.mjs +1 -1
- package/node/storage_plugins.mjs +2 -2
- package/overlay/index.mjs +1 -1
- package/package.json +1 -1
- package/registries/event_type.mjs +2 -2
- package/registries/room_provider.mjs +2 -2
- package/reputation/engine.mjs +17 -15
- package/schemas/discovery.mjs +5 -5
- package/schemas/federation_pull.mjs +16 -24
- package/schemas/part_query.mjs +4 -4
- package/timeline/verify_remote.mjs +4 -4
- package/transport/group_link_set.mjs +3 -3
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +51 -20
- package/transport/mesh_keepalive.mjs +1 -1
- package/transport/node_scope/wire.mjs +1 -1
- package/transport/peer_health.mjs +127 -0
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +6 -6
- package/transport/runtime_bootstrap.mjs +12 -12
- package/trust_graph/cache.mjs +2 -3
- package/trust_graph/engine.mjs +1 -1
- package/trust_graph/registry.mjs +2 -2
- package/trust_graph/send.mjs +1 -1
- package/utils/async_mutex.mjs +1 -1
- package/wire/ingress.mjs +1 -1
- package/wire/part/ingress.mjs +6 -6
- package/wire/part/query.mjs +3 -3
- package/wire/volatile_signature.mjs +4 -9
|
@@ -17,7 +17,7 @@ export function normalizeLanHosts(input) {
|
|
|
17
17
|
/** @type {string[]} */
|
|
18
18
|
const out = []
|
|
19
19
|
for (const item of arr) {
|
|
20
|
-
const host = String(item || '')
|
|
20
|
+
const host = String(item || '')
|
|
21
21
|
if (!host || !IPV4_RE.test(host) || seen.has(host)) continue
|
|
22
22
|
seen.add(host)
|
|
23
23
|
out.push(host)
|
|
@@ -68,7 +68,7 @@ export function listMulticastIpv4Addresses() {
|
|
|
68
68
|
if (!ifaces) continue
|
|
69
69
|
for (const iface of ifaces) {
|
|
70
70
|
if (iface.internal || !isIpv4Family(iface.family)) continue
|
|
71
|
-
const addr = String(iface.address || '')
|
|
71
|
+
const addr = String(iface.address || '')
|
|
72
72
|
if (!addr || seen.has(addr)) continue
|
|
73
73
|
seen.add(addr)
|
|
74
74
|
addrs.push(addr)
|
|
@@ -20,7 +20,7 @@ const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
|
|
|
20
20
|
*/
|
|
21
21
|
export function noteLanPeerHint(nodeHash, endpoint) {
|
|
22
22
|
const hash = normalizeHex64(nodeHash)
|
|
23
|
-
const host = String(endpoint?.host || '')
|
|
23
|
+
const host = String(endpoint?.host || '')
|
|
24
24
|
const port = normalizeTcpPort(endpoint?.port)
|
|
25
25
|
if (!hash || !host || !port) return
|
|
26
26
|
const existing = hints.get(hash)?.endpoints ?? []
|
package/discovery/nostr.mjs
CHANGED
|
@@ -82,13 +82,12 @@ export function noteNostrVisibleNode(nodeHash, now = Date.now()) {
|
|
|
82
82
|
* @returns {void}
|
|
83
83
|
*/
|
|
84
84
|
export function noteNostrGroupVisibleNode(roomSecret, nodeHash, now = Date.now()) {
|
|
85
|
-
const key = String(roomSecret || '')
|
|
86
85
|
const hash = normalizeHex64(nodeHash)
|
|
87
|
-
if (!
|
|
88
|
-
let pool = visibleByGroup.get(
|
|
86
|
+
if (!roomSecret || !isHex64(hash)) return
|
|
87
|
+
let pool = visibleByGroup.get(roomSecret)
|
|
89
88
|
if (!pool) {
|
|
90
89
|
pool = new Map()
|
|
91
|
-
visibleByGroup.set(
|
|
90
|
+
visibleByGroup.set(roomSecret, pool)
|
|
92
91
|
}
|
|
93
92
|
pool.set(hash, now)
|
|
94
93
|
}
|
|
@@ -109,11 +108,10 @@ export function listNostrVisibleNodeHashes(now = Date.now(), ttlMs = ADVERT_TTL_
|
|
|
109
108
|
* @returns {string[]} 该群可见 nodeHash
|
|
110
109
|
*/
|
|
111
110
|
export function listNostrGroupVisibleNodeHashes(roomSecret, now = Date.now(), ttlMs = ADVERT_TTL_MS) {
|
|
112
|
-
const
|
|
113
|
-
const pool = visibleByGroup.get(key)
|
|
111
|
+
const pool = visibleByGroup.get(roomSecret)
|
|
114
112
|
if (!pool) return []
|
|
115
113
|
const out = listPoolHashes(pool, now, ttlMs)
|
|
116
|
-
if (!pool.size) visibleByGroup.delete(
|
|
114
|
+
if (!pool.size) visibleByGroup.delete(roomSecret)
|
|
117
115
|
return out
|
|
118
116
|
}
|
|
119
117
|
|
|
@@ -130,12 +128,11 @@ export async function acceptNostrAdvert(rendezvousKey, bytes, options = {}) {
|
|
|
130
128
|
const hash = ingested.verifiedNodeHash
|
|
131
129
|
const skipHash = options.skipNodeHash ? normalizeHex64(options.skipNodeHash) : null
|
|
132
130
|
if (skipHash && hash === skipHash) return hash
|
|
133
|
-
const roomSecret = options
|
|
131
|
+
const { roomSecret } = options
|
|
134
132
|
let firstSeen = true
|
|
135
133
|
if (roomSecret) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
noteNostrGroupVisibleNode(key, hash)
|
|
134
|
+
firstSeen = !visibleByGroup.get(roomSecret)?.has(hash)
|
|
135
|
+
noteNostrGroupVisibleNode(roomSecret, hash)
|
|
139
136
|
}
|
|
140
137
|
else {
|
|
141
138
|
firstSeen = !visibleByHash.has(hash)
|
|
@@ -188,8 +185,8 @@ function dropWebSocket(ws) {
|
|
|
188
185
|
function dedupeRelayUrls(urls) {
|
|
189
186
|
const seen = new Set()
|
|
190
187
|
return (urls || [])
|
|
191
|
-
.map(url => url
|
|
192
|
-
.filter(
|
|
188
|
+
.map(url => String(url || ''))
|
|
189
|
+
.filter(url => url && !seen.has(url) && (seen.add(url), true))
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
/**
|
|
@@ -502,7 +499,7 @@ function releaseSharedRelaySub(relayUrl, subscriptionId) {
|
|
|
502
499
|
const session = sharedRelaySessions.get(relayUrl)
|
|
503
500
|
if (!session) return
|
|
504
501
|
session.subs.delete(subscriptionId)
|
|
505
|
-
const ws = session
|
|
502
|
+
const { ws } = session
|
|
506
503
|
if (ws?.readyState === WebSocket.OPEN)
|
|
507
504
|
try { ws.send(JSON.stringify(['CLOSE', subscriptionId])) } catch { /* ignore */ }
|
|
508
505
|
if (session.subs.size) return
|
|
@@ -521,7 +518,7 @@ function releaseSharedRelaySub(relayUrl, subscriptionId) {
|
|
|
521
518
|
*/
|
|
522
519
|
function registerSharedRelaySub(session, subscriptionId, filter, onEvent) {
|
|
523
520
|
session.subs.set(subscriptionId, { filter, onEvent })
|
|
524
|
-
const ws = session
|
|
521
|
+
const { ws } = session
|
|
525
522
|
if (ws?.readyState !== WebSocket.OPEN) return
|
|
526
523
|
try { ws.send(JSON.stringify(['REQ', subscriptionId, filter])) } catch { /* ignore */ }
|
|
527
524
|
}
|
|
@@ -568,7 +565,7 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
568
565
|
* @returns {string[]} 去重后的中继 URL 列表
|
|
569
566
|
*/
|
|
570
567
|
const resolveRelayUrls = () => {
|
|
571
|
-
if (
|
|
568
|
+
if (options.getRelayUrls) {
|
|
572
569
|
const urls = options.getRelayUrls()
|
|
573
570
|
return dedupeRelayUrls(urls == null ? DEFAULT_RELAY_URLS : urls)
|
|
574
571
|
}
|
|
@@ -676,11 +673,10 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
676
673
|
* @returns {() => void} 取消 listener
|
|
677
674
|
*/
|
|
678
675
|
function ensureGroupSubscription(roomSecret, listener) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
roomSecret: key,
|
|
676
|
+
if (!roomSecret) return () => { }
|
|
677
|
+
return ensureAdvertSubscription('group:' + roomSecret, {
|
|
678
|
+
rendezvousKey: groupRendezvousKey(roomSecret),
|
|
679
|
+
roomSecret,
|
|
684
680
|
}, listener)
|
|
685
681
|
}
|
|
686
682
|
|
|
@@ -828,10 +824,9 @@ export function createNostrDiscoveryProvider(options = {}) {
|
|
|
828
824
|
* @returns {Promise<() => void>} 停止群 presence 广播
|
|
829
825
|
*/
|
|
830
826
|
async startGroupPresence(roomSecret, getBeacon) {
|
|
831
|
-
const
|
|
832
|
-
const rendezvousKey = groupRendezvousKey(key)
|
|
827
|
+
const rendezvousKey = groupRendezvousKey(roomSecret)
|
|
833
828
|
const abortController = new AbortController()
|
|
834
|
-
ensureGroupSubscription(
|
|
829
|
+
ensureGroupSubscription(roomSecret)
|
|
835
830
|
/**
|
|
836
831
|
* @returns {Promise<void>}
|
|
837
832
|
*/
|
|
@@ -23,28 +23,11 @@ export const ENTITY_KEY_REVOKE_DOMAIN = 'fount-entity-key-revoke'
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* @param {unknown} recoveryPubKeyHex 64 位十六进制 recovery 公钥
|
|
27
|
-
* @returns {string} 稳定 subjectHash(entityHash 后半)
|
|
28
|
-
*/
|
|
29
|
-
export function recoverySubjectHashFromPubKeyHex(recoveryPubKeyHex) {
|
|
30
|
-
return hashFromPubKeyHex(recoveryPubKeyHex)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {unknown} activePubKeyHex 64 位十六进制 活跃公钥
|
|
35
|
-
* @returns {string} 时间线 sender(pubKeyHash)
|
|
36
|
-
*/
|
|
37
|
-
export function activeSenderHashFromPubKeyHex(activePubKeyHex) {
|
|
38
|
-
return hashFromPubKeyHex(activePubKeyHex)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {string} recoveryPubKeyHex recovery 公钥
|
|
43
26
|
* @param {string} activePubKeyHex 初始活跃公钥
|
|
44
27
|
* @param {number} [validFrom] 生效时间
|
|
45
28
|
* @returns {EntityKeyHistoryEntry[]} 创世链
|
|
46
29
|
*/
|
|
47
|
-
export function createGenesisKeyHistory(
|
|
30
|
+
export function createGenesisKeyHistory(activePubKeyHex, validFrom = Date.now()) {
|
|
48
31
|
return [{
|
|
49
32
|
generation: 0,
|
|
50
33
|
activePubKeyHex: normalizeHex64(activePubKeyHex),
|
|
@@ -76,17 +59,15 @@ export function isActiveGenerationRevoked(keyHistory, generation) {
|
|
|
76
59
|
|
|
77
60
|
/**
|
|
78
61
|
* @param {EntityKeyHistoryEntry[]} keyHistory 密钥历史
|
|
79
|
-
* @param {string} recoveryPubKeyHex recovery 公钥
|
|
80
62
|
* @param {string} senderPubKeyHash 事件 sender(64 hex pubKeyHash)
|
|
81
63
|
* @returns {boolean} sender 是否为未吊销的活跃钥
|
|
82
64
|
*/
|
|
83
|
-
export function isValidActiveSender(keyHistory,
|
|
65
|
+
export function isValidActiveSender(keyHistory, senderPubKeyHash) {
|
|
84
66
|
const sender = normalizeHex64(senderPubKeyHash)
|
|
85
67
|
if (!isHex64(sender)) return false
|
|
86
|
-
void recoveryPubKeyHex
|
|
87
68
|
for (const entry of keyHistory || []) {
|
|
88
69
|
if (isActiveGenerationRevoked(keyHistory, entry.generation)) continue
|
|
89
|
-
if (
|
|
70
|
+
if (hashFromPubKeyHex(entry.activePubKeyHex) === sender)
|
|
90
71
|
return true
|
|
91
72
|
}
|
|
92
73
|
return false
|
|
@@ -98,7 +79,7 @@ export function isValidActiveSender(keyHistory, recoveryPubKeyHex, senderPubKeyH
|
|
|
98
79
|
* @returns {boolean} 是否为 recovery 钥签名
|
|
99
80
|
*/
|
|
100
81
|
export function isRecoverySender(recoveryPubKeyHex, senderPubKeyHash) {
|
|
101
|
-
return
|
|
82
|
+
return hashFromPubKeyHex(recoveryPubKeyHex) === normalizeHex64(senderPubKeyHash)
|
|
102
83
|
}
|
|
103
84
|
|
|
104
85
|
/**
|
|
@@ -183,7 +164,7 @@ export function entityKeyRevokeSignBytes(revokeBody) {
|
|
|
183
164
|
revokeGenerations: (revokeBody.revokeGenerations || []).map(Number),
|
|
184
165
|
newGeneration: Number(revokeBody.newGeneration),
|
|
185
166
|
activePubKeyHex: normalizeHex64(revokeBody.activePubKeyHex || ''),
|
|
186
|
-
entityHash: String(revokeBody.entityHash || '')
|
|
167
|
+
entityHash: String(revokeBody.entityHash || ''),
|
|
187
168
|
}
|
|
188
169
|
return Buffer.from(`${ENTITY_KEY_REVOKE_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
|
|
189
170
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 群级消息限速纯函数(无 I/O,供单测与 governance 复用)。
|
|
3
3
|
*/
|
|
4
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @param {object} event DAG 事件
|
|
7
8
|
* @returns {string} 限速实体键
|
|
8
9
|
*/
|
|
9
10
|
export function messageRateEntityKey(event) {
|
|
10
|
-
const charId = String(event?.charId || '')
|
|
11
|
+
const charId = String(event?.charId || '')
|
|
11
12
|
if (charId) return `char:${charId}`
|
|
12
|
-
return
|
|
13
|
+
return normalizeHex64(event?.sender)
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -102,15 +102,6 @@ export function resolvePartQueryState(dependencies = {}) {
|
|
|
102
102
|
return dependencies.state || defaultState
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* @param {string} partpath part 路径
|
|
107
|
-
* @param {string} kind 查询标签
|
|
108
|
-
* @returns {string} handler 键
|
|
109
|
-
*/
|
|
110
|
-
function handlerKey(partpath, kind) {
|
|
111
|
-
return compositeKey(partpath, kind)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
105
|
/**
|
|
115
106
|
* Shell 注册 kind 语义处理器(怎么匹配、返回什么)。
|
|
116
107
|
* @param {string} partpath part 路径
|
|
@@ -120,7 +111,7 @@ function handlerKey(partpath, kind) {
|
|
|
120
111
|
* @returns {void}
|
|
121
112
|
*/
|
|
122
113
|
export function registerQueryInboundHandler(partpath, kind, handler, state = defaultState) {
|
|
123
|
-
state.handlers.set(
|
|
114
|
+
state.handlers.set(compositeKey(partpath, kind), handler)
|
|
124
115
|
}
|
|
125
116
|
|
|
126
117
|
/**
|
|
@@ -144,12 +135,10 @@ export function resolvePartQueryHopTimeoutMs(ttl, tunables = partQueryTunables)
|
|
|
144
135
|
export function mergeQueryRows(lists, maxHits, rowKey) {
|
|
145
136
|
const out = []
|
|
146
137
|
const seen = new Set()
|
|
147
|
-
const keyOf =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
catch { return `\0${out.length}` }
|
|
152
|
-
}
|
|
138
|
+
const keyOf = rowKey || (row => {
|
|
139
|
+
try { return JSON.stringify(row) }
|
|
140
|
+
catch { return `\0${out.length}` }
|
|
141
|
+
})
|
|
153
142
|
for (const list of lists) {
|
|
154
143
|
if (!Array.isArray(list)) continue
|
|
155
144
|
for (const row of list) {
|
|
@@ -172,7 +161,7 @@ export function mergeQueryRows(lists, maxHits, rowKey) {
|
|
|
172
161
|
* @returns {Promise<unknown[]>} 本地 rows
|
|
173
162
|
*/
|
|
174
163
|
async function runLocalHandler(state, queryContext, partpath, kind, query) {
|
|
175
|
-
const handler = state.handlers.get(
|
|
164
|
+
const handler = state.handlers.get(compositeKey(partpath, kind))
|
|
176
165
|
if (!handler) return []
|
|
177
166
|
return await handler(queryContext, query) || []
|
|
178
167
|
}
|
|
@@ -260,7 +249,7 @@ export async function processIncomingPartQueryRequest(wireContext, wire, request
|
|
|
260
249
|
const state = resolvePartQueryState(dependencies)
|
|
261
250
|
const nodeHashOf = dependencies.getNodeHash || getNodeHash
|
|
262
251
|
const now = dependencies.now || Date.now
|
|
263
|
-
const username =
|
|
252
|
+
const username = wireContext.replicaUsername || ''
|
|
264
253
|
|
|
265
254
|
const cached = state.cache.get(request.partpath, request.kind, request.query, now())
|
|
266
255
|
if (cached) {
|
|
@@ -284,7 +273,7 @@ export async function processIncomingPartQueryRequest(wireContext, wire, request
|
|
|
284
273
|
}
|
|
285
274
|
|
|
286
275
|
const selfHash = nodeHashOf()
|
|
287
|
-
const exclude = new Set([selfHash, request.originNodeHash,
|
|
276
|
+
const exclude = new Set([selfHash, request.originNodeHash, peerId].filter(Boolean))
|
|
288
277
|
const forwardPayload = { ...request, ttl: nextTtl }
|
|
289
278
|
|
|
290
279
|
/** @type {RelayPending} */
|
|
@@ -332,7 +321,7 @@ export async function processIncomingPartQueryRequest(wireContext, wire, request
|
|
|
332
321
|
*/
|
|
333
322
|
export function handleIncomingPartQueryResponse(response, peerId = '', dependencies = {}) {
|
|
334
323
|
const state = resolvePartQueryState(dependencies)
|
|
335
|
-
const responderKey =
|
|
324
|
+
const responderKey = peerId || response.fromNodeHash
|
|
336
325
|
const relay = state.relayPending.get(response.requestId)
|
|
337
326
|
if (relay) {
|
|
338
327
|
if (responderKey) {
|
package/files/assemble.mjs
CHANGED
|
@@ -171,7 +171,7 @@ export function buildFileManifest(parameters) {
|
|
|
171
171
|
} = parameters
|
|
172
172
|
const enc = encryptPlaintextToParts(plaintext, ceMode)
|
|
173
173
|
const manifest = normalizeFileManifest({
|
|
174
|
-
ownerEntityHash
|
|
174
|
+
ownerEntityHash,
|
|
175
175
|
logicalPath: logicalPath.replace(/^\/+/, ''),
|
|
176
176
|
name: name || logicalPath.split('/').pop() || 'file',
|
|
177
177
|
mimeType,
|
|
@@ -204,7 +204,7 @@ export function buildFileManifestFromEnc(parameters, enc) {
|
|
|
204
204
|
meta,
|
|
205
205
|
} = parameters
|
|
206
206
|
const manifest = normalizeFileManifest({
|
|
207
|
-
ownerEntityHash
|
|
207
|
+
ownerEntityHash,
|
|
208
208
|
logicalPath: logicalPath.replace(/^\/+/, ''),
|
|
209
209
|
name: name || logicalPath.split('/').pop() || 'file',
|
|
210
210
|
mimeType,
|
|
@@ -229,7 +229,7 @@ export function buildFileManifestFromEnc(parameters, enc) {
|
|
|
229
229
|
export function vaultWrapDescriptor(entityHash, fileId, contentKey, H) {
|
|
230
230
|
return {
|
|
231
231
|
type: 'vault-wrap',
|
|
232
|
-
entityHash
|
|
232
|
+
entityHash,
|
|
233
233
|
fileId,
|
|
234
234
|
wrappedKey: wrapContentKey(contentKey, H, fileId),
|
|
235
235
|
}
|
|
@@ -165,7 +165,7 @@ export function createManifestPlaintextStream(manifest, partStreams, contentKey)
|
|
|
165
165
|
}
|
|
166
166
|
if (manifest.contentHash) {
|
|
167
167
|
const got = digest.digest('hex')
|
|
168
|
-
if (got !==
|
|
168
|
+
if (got !== manifest.contentHash) {
|
|
169
169
|
this.destroy(new Error('contentHash mismatch'))
|
|
170
170
|
return
|
|
171
171
|
}
|
package/files/chunk/fetch.mjs
CHANGED
|
@@ -34,8 +34,7 @@ const chunkInflight = createInflightTable({
|
|
|
34
34
|
* @returns {Promise<Uint8Array | null>} 密文块
|
|
35
35
|
*/
|
|
36
36
|
export async function fetchChunk(context) {
|
|
37
|
-
const hash =
|
|
38
|
-
const { username } = context
|
|
37
|
+
const { username, ciphertextHash: hash } = context
|
|
39
38
|
if (!hash || !username) return null
|
|
40
39
|
|
|
41
40
|
if (await hasChunk(hash))
|
|
@@ -4,43 +4,6 @@ import { resolvePendingManifestFetch } from '../manifest/pending.mjs'
|
|
|
4
4
|
import { handleIncomingChunkGet } from './fetch.mjs'
|
|
5
5
|
import { resolvePendingChunkFetch } from './pending.mjs'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* 群联邦 / TrustGraph 路径:响应带 requestId 的 fed_chunk_get。
|
|
9
|
-
* @param {object} data 入站 fed_chunk_get
|
|
10
|
-
* @param {string} peerId 对端 id
|
|
11
|
-
* @param {(resp: object, peerId: string) => void} sendChunkData 发送 fed_chunk_data
|
|
12
|
-
* @returns {Promise<void>}
|
|
13
|
-
*/
|
|
14
|
-
export async function handleFedChunkGetIngress(data, peerId, sendChunkData) {
|
|
15
|
-
await handleIncomingChunkGet(data, sendChunkData, peerId)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} data 入站 fed_chunk_data(含 requestId 时 resolve pending fetch)
|
|
20
|
-
* @returns {boolean} 是否命中 pending
|
|
21
|
-
*/
|
|
22
|
-
export function handleFedChunkDataIngress(data) {
|
|
23
|
-
return resolvePendingChunkFetch(data)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {object} data 入站 fed_manifest_get
|
|
28
|
-
* @param {string} peerId 对端 id
|
|
29
|
-
* @param {(resp: object, peerId: string) => void} sendManifestData 发送 fed_manifest_data
|
|
30
|
-
* @returns {Promise<void>}
|
|
31
|
-
*/
|
|
32
|
-
export async function handleFedManifestGetIngress(data, peerId, sendManifestData) {
|
|
33
|
-
await handleIncomingManifestGet(data, sendManifestData, peerId)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {object} data 入站 fed_manifest_data
|
|
38
|
-
* @returns {Promise<boolean>} 是否命中 pending
|
|
39
|
-
*/
|
|
40
|
-
export function handleFedManifestDataIngress(data) {
|
|
41
|
-
return resolvePendingManifestFetch(data)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
7
|
/**
|
|
45
8
|
* @param {(resp: object, peerId: string) => void} sendData 发送
|
|
46
9
|
* @param {{ enqueue: (prio: number, cleanup: () => void) => void }} [fedOut] 出站限速队列
|
|
@@ -83,22 +46,22 @@ export function attachTrustGraphFedChunkResponder(room, fedOut, guardGet, rtcLim
|
|
|
83
46
|
getChunkGet((data, peerId) => {
|
|
84
47
|
if (guardGet && !guardGet(roomKey, 'fed_chunk_get', rtcLimits)) return
|
|
85
48
|
if (!data?.requestId) return
|
|
86
|
-
void
|
|
49
|
+
void handleIncomingChunkGet(data, sendChunk, peerId)
|
|
87
50
|
})
|
|
88
51
|
|
|
89
52
|
getChunkData(data => {
|
|
90
53
|
if (!data?.requestId) return
|
|
91
|
-
|
|
54
|
+
resolvePendingChunkFetch(data)
|
|
92
55
|
})
|
|
93
56
|
|
|
94
57
|
getManifestGet((data, peerId) => {
|
|
95
58
|
if (guardGet && !guardGet(roomKey, 'fed_manifest_get', rtcLimits)) return
|
|
96
59
|
if (!data?.requestId) return
|
|
97
|
-
void
|
|
60
|
+
void handleIncomingManifestGet(data, sendManifest, peerId)
|
|
98
61
|
})
|
|
99
62
|
|
|
100
63
|
getManifestData(data => {
|
|
101
64
|
if (!data?.requestId) return
|
|
102
|
-
|
|
65
|
+
resolvePendingManifestFetch(data)
|
|
103
66
|
})
|
|
104
67
|
}
|
package/files/chunk/store.mjs
CHANGED
|
@@ -19,9 +19,8 @@ export function chunkStoreRoot() {
|
|
|
19
19
|
* @returns {string} 块文件绝对路径
|
|
20
20
|
*/
|
|
21
21
|
export function chunkStorePath(hash) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return join(chunkStoreRoot(), chunkHash.slice(0, 2), `${chunkHash}.bin`)
|
|
22
|
+
if (!isHex64(hash)) throw new Error('invalid chunk hash')
|
|
23
|
+
return join(chunkStoreRoot(), hash.slice(0, 2), `${hash}.bin`)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
/**
|
package/files/chunk/verify.mjs
CHANGED
|
@@ -7,9 +7,8 @@ import { sha256Hex } from '../../crypto/crypto.mjs'
|
|
|
7
7
|
* @returns {boolean} 是否与 hash 一致
|
|
8
8
|
*/
|
|
9
9
|
export function chunkBytesMatchHash(chunkHash, data) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return sha256Hex(data) === hash
|
|
10
|
+
if (!isHex64(chunkHash) || !data?.byteLength) return false
|
|
11
|
+
return sha256Hex(data) === chunkHash
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
/**
|
package/files/evfs.mjs
CHANGED
|
@@ -197,7 +197,7 @@ export async function putFileManifestFromStream(parameters) {
|
|
|
197
197
|
const enc = await encryptReadableToParts(readable, ceMode, async part =>
|
|
198
198
|
putChunk(part.hash, part.raw), plainSize)
|
|
199
199
|
const manifest = normalizeFileManifest({
|
|
200
|
-
ownerEntityHash
|
|
200
|
+
ownerEntityHash,
|
|
201
201
|
logicalPath: logicalPath.replace(/^\/+/, ''),
|
|
202
202
|
name: name || logicalPath.split('/').pop() || 'file',
|
|
203
203
|
mimeType: mimeType || 'application/octet-stream',
|
package/files/evfs_ref.mjs
CHANGED
|
@@ -10,9 +10,8 @@ export const EVFS_SCHEME = 'evfs:'
|
|
|
10
10
|
* @returns {string} evfs URI 引用
|
|
11
11
|
*/
|
|
12
12
|
export function formatEvfsRef(entityHash, logicalPath) {
|
|
13
|
-
const eh = String(entityHash).trim().toLowerCase()
|
|
14
13
|
const safePath = assertSafeEvfsLogicalPath(logicalPath)
|
|
15
|
-
return `${EVFS_SCHEME}//${
|
|
14
|
+
return `${EVFS_SCHEME}//${entityHash}/${safePath}`
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -24,8 +23,8 @@ export function parseEvfsRef(ref) {
|
|
|
24
23
|
try {
|
|
25
24
|
const url = new URL(ref)
|
|
26
25
|
if (url.protocol !== 'evfs:') return null
|
|
27
|
-
const entityHash =
|
|
28
|
-
const logicalPath =
|
|
26
|
+
const entityHash = url.hostname
|
|
27
|
+
const logicalPath = url.pathname.replace(/^\/+/, '')
|
|
29
28
|
if (!isEntityHash128(entityHash)) return null
|
|
30
29
|
return { entityHash, logicalPath: assertSafeEvfsLogicalPath(logicalPath) }
|
|
31
30
|
}
|
package/files/fetch_fanout.mjs
CHANGED
|
@@ -10,12 +10,12 @@ function fetchPeerTargets() {
|
|
|
10
10
|
/** @type {Set<string>} */
|
|
11
11
|
const targets = new Set()
|
|
12
12
|
for (const { nodeHash } of listLinks())
|
|
13
|
-
if (nodeHash) targets.add(
|
|
13
|
+
if (nodeHash) targets.add(nodeHash)
|
|
14
14
|
const net = loadNetwork()
|
|
15
15
|
for (const nodeHash of [...net.trustedPeers || [], ...net.explorePeers || []])
|
|
16
|
-
if (nodeHash) targets.add(
|
|
16
|
+
if (nodeHash) targets.add(nodeHash)
|
|
17
17
|
for (const hint of net.hints || [])
|
|
18
|
-
if (hint?.nodeHash) targets.add(
|
|
18
|
+
if (hint?.nodeHash) targets.add(hint.nodeHash)
|
|
19
19
|
return [...targets]
|
|
20
20
|
}
|
|
21
21
|
|
package/files/manifest/fetch.mjs
CHANGED
|
@@ -31,9 +31,8 @@ const manifestInflight = createInflightTable({
|
|
|
31
31
|
* @returns {Promise<import('./normalize.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
|
|
32
32
|
*/
|
|
33
33
|
export async function fetchPublicManifest(context) {
|
|
34
|
-
const ownerEntityHash =
|
|
35
|
-
const logicalPath =
|
|
36
|
-
const { username } = context
|
|
34
|
+
const { ownerEntityHash, username } = context
|
|
35
|
+
const logicalPath = context.logicalPath.replace(/^\/+/, '')
|
|
37
36
|
if (!ownerEntityHash || !logicalPath || !username) return null
|
|
38
37
|
|
|
39
38
|
const timeoutMs = Number(context.timeoutMs) > 0
|
|
@@ -40,9 +40,9 @@ const TRANSFER_TYPES = new Set(['public', 'file-master-key-wrap', 'vault-wrap',
|
|
|
40
40
|
*/
|
|
41
41
|
export function normalizeFileManifest(input) {
|
|
42
42
|
if (!input || typeof input !== 'object') return null
|
|
43
|
-
const ownerEntityHash = String(input.ownerEntityHash || '')
|
|
43
|
+
const ownerEntityHash = String(input.ownerEntityHash || '')
|
|
44
44
|
if (!isEntityHash128(ownerEntityHash)) return null
|
|
45
|
-
const logicalPath = String(input.logicalPath || '').
|
|
45
|
+
const logicalPath = String(input.logicalPath || '').replace(/^\/+/, '').replace(/\\/g, '/')
|
|
46
46
|
if (!logicalPath) return null
|
|
47
47
|
const ceMode = String(input.ceMode || 'convergent')
|
|
48
48
|
if (!CE_MODES.has(ceMode)) return null
|
|
@@ -51,10 +51,10 @@ export function normalizeFileManifest(input) {
|
|
|
51
51
|
.map(part => {
|
|
52
52
|
/** @type {ManifestPart} */
|
|
53
53
|
const out = {
|
|
54
|
-
hash: String(part?.hash || '')
|
|
54
|
+
hash: String(part?.hash || ''),
|
|
55
55
|
size: Number(part?.size) || 0,
|
|
56
56
|
}
|
|
57
|
-
const partContentHash = String(part?.contentHash || '')
|
|
57
|
+
const partContentHash = String(part?.contentHash || '')
|
|
58
58
|
if (isHex64(partContentHash)) out.contentHash = partContentHash
|
|
59
59
|
return out
|
|
60
60
|
})
|
|
@@ -69,7 +69,7 @@ export function normalizeFileManifest(input) {
|
|
|
69
69
|
name: String(input.name || logicalPath.split('/').pop() || 'file'),
|
|
70
70
|
mimeType: String(input.mimeType || 'application/octet-stream'),
|
|
71
71
|
size: Number(input.size) || 0,
|
|
72
|
-
contentHash: String(input.contentHash || '')
|
|
72
|
+
contentHash: String(input.contentHash || ''),
|
|
73
73
|
ceMode: /** @type {CeMode} */ ceMode,
|
|
74
74
|
parts,
|
|
75
75
|
transferKeyDescriptor,
|
|
@@ -83,7 +83,7 @@ export function normalizeFileManifest(input) {
|
|
|
83
83
|
*/
|
|
84
84
|
export function normalizeTransferKeyDescriptor(input) {
|
|
85
85
|
if (!input || typeof input !== 'object') return null
|
|
86
|
-
const type = String(input.type || '')
|
|
86
|
+
const type = String(input.type || '')
|
|
87
87
|
if (!TRANSFER_TYPES.has(type)) return null
|
|
88
88
|
/** @type {TransferKeyDescriptor} */
|
|
89
89
|
const out = { type: /** @type {TransferKeyDescriptor['type']} */ type }
|
|
@@ -93,9 +93,9 @@ export function normalizeTransferKeyDescriptor(input) {
|
|
|
93
93
|
ciphertext: String(input.wrappedKey.ciphertext || ''),
|
|
94
94
|
authTag: String(input.wrappedKey.authTag || ''),
|
|
95
95
|
}
|
|
96
|
-
if (input.groupId) out.groupId = String(input.groupId)
|
|
97
|
-
if (input.fileId) out.fileId = String(input.fileId)
|
|
98
|
-
if (input.entityHash) out.entityHash = String(input.entityHash)
|
|
96
|
+
if (input.groupId) out.groupId = String(input.groupId)
|
|
97
|
+
if (input.fileId) out.fileId = String(input.fileId)
|
|
98
|
+
if (input.entityHash) out.entityHash = String(input.entityHash)
|
|
99
99
|
if (input.keyGeneration != null) out.keyGeneration = Number(input.keyGeneration) || 0
|
|
100
100
|
return out
|
|
101
101
|
}
|
|
@@ -17,10 +17,7 @@ export const pendingManifestFetches = table.pending
|
|
|
17
17
|
* @returns {string} 期望键
|
|
18
18
|
*/
|
|
19
19
|
export function manifestFetchExpectedKey(ownerEntityHash, logicalPath) {
|
|
20
|
-
return compositeKey(
|
|
21
|
-
String(ownerEntityHash || '').trim().toLowerCase(),
|
|
22
|
-
String(logicalPath || '').trim().replace(/^\/+/, ''),
|
|
23
|
-
)
|
|
20
|
+
return compositeKey(ownerEntityHash, logicalPath.replace(/^\/+/, ''))
|
|
24
21
|
}
|
|
25
22
|
|
|
26
23
|
/**
|
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import { canonicalStringify } from '../../core/canonical_json.mjs'
|
|
4
4
|
import { hashFromPubKeyHex, parseEntityHash } from '../../core/entity_id.mjs'
|
|
5
5
|
import { assertSafeEvfsLogicalPath } from '../../core/evfs_logical_path.mjs'
|
|
6
|
-
import { isHex64, normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
|
+
import { isHex64, isSignatureHex128, normalizeHex64 } from '../../core/hexIds.mjs'
|
|
7
7
|
import { sign, verify } from '../../crypto/crypto.mjs'
|
|
8
8
|
import { putFileManifest, saveFileManifest } from '../evfs.mjs'
|
|
9
9
|
|
|
@@ -19,14 +19,14 @@ export const ENTITY_PUBLIC_MANIFEST_DOMAIN = 'fount-entity-public-manifest'
|
|
|
19
19
|
export function publicManifestSignBytes(fields) {
|
|
20
20
|
return Buffer.from(canonicalStringify([
|
|
21
21
|
ENTITY_PUBLIC_MANIFEST_DOMAIN,
|
|
22
|
-
|
|
22
|
+
fields.ownerEntityHash || '',
|
|
23
23
|
assertSafeEvfsLogicalPath(fields.logicalPath),
|
|
24
24
|
Number(fields.publishedAt) || 0,
|
|
25
|
-
|
|
25
|
+
fields.contentHash || '',
|
|
26
26
|
Number(fields.size) || 0,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
fields.mimeType || '',
|
|
28
|
+
fields.name || '',
|
|
29
|
+
fields.ceMode || '',
|
|
30
30
|
fields.parts || [],
|
|
31
31
|
]), 'utf8')
|
|
32
32
|
}
|
|
@@ -74,8 +74,8 @@ export async function verifySignedPublicManifest(input) {
|
|
|
74
74
|
if (!publicSig || typeof publicSig !== 'object') return null
|
|
75
75
|
const publishedAt = Number(publicSig.publishedAt) || 0
|
|
76
76
|
const pubKeyHex = normalizeHex64(publicSig.pubKeyHex)
|
|
77
|
-
const sigHex = String(publicSig.sigHex || '')
|
|
78
|
-
if (!isHex64(pubKeyHex) ||
|
|
77
|
+
const sigHex = String(publicSig.sigHex || '')
|
|
78
|
+
if (!isHex64(pubKeyHex) || !isSignatureHex128(sigHex) || publishedAt <= 0) return null
|
|
79
79
|
|
|
80
80
|
const parsed = parseEntityHash(manifest.ownerEntityHash)
|
|
81
81
|
if (!parsed) return null
|
package/files/transfer_key.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export async function resolveContentKey(descriptor, manifest, dependencies = {})
|
|
|
26
26
|
if (type === 'file-master-key-wrap') {
|
|
27
27
|
const { groupId, fileId } = descriptor
|
|
28
28
|
if (!groupId || !fileId || !descriptor.wrappedKey || !dependencies.getGroupFileMasterKey) return null
|
|
29
|
-
const groupKey = await dependencies.getGroupFileMasterKey(
|
|
29
|
+
const groupKey = await dependencies.getGroupFileMasterKey(groupId, descriptor.keyGeneration)
|
|
30
30
|
if (!groupKey) return null
|
|
31
31
|
return unwrapContentKey(descriptor.wrappedKey, groupKey, fileId)
|
|
32
32
|
}
|
|
@@ -34,7 +34,7 @@ export async function resolveContentKey(descriptor, manifest, dependencies = {})
|
|
|
34
34
|
if (type === 'vault-wrap') {
|
|
35
35
|
const { entityHash, fileId } = descriptor
|
|
36
36
|
if (!entityHash || !fileId || !descriptor.wrappedKey || !dependencies.getVaultMasterKey) return null
|
|
37
|
-
const vaultKey = await dependencies.getVaultMasterKey(
|
|
37
|
+
const vaultKey = await dependencies.getVaultMasterKey(entityHash)
|
|
38
38
|
if (!vaultKey) return null
|
|
39
39
|
return unwrapContentKey(descriptor.wrappedKey, vaultKey, fileId)
|
|
40
40
|
}
|
|
@@ -85,7 +85,7 @@ export async function assembleManifestPlaintext(manifest, partBytes, dependencie
|
|
|
85
85
|
}
|
|
86
86
|
const merged = Buffer.concat(plains)
|
|
87
87
|
if (manifest.contentHash)
|
|
88
|
-
if (createHash('sha256').update(merged).digest('hex') !== manifest.contentHash
|
|
88
|
+
if (createHash('sha256').update(merged).digest('hex') !== manifest.contentHash) return null
|
|
89
89
|
|
|
90
90
|
return merged
|
|
91
91
|
}
|