@steve02081504/fount-p2p 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/core/bytes_codec.mjs +0 -27
- package/core/constants.mjs +0 -31
- package/core/hexIds.mjs +0 -13
- package/{entity → core}/logical_entity.mjs +2 -1
- package/crypto/key.mjs +14 -15
- package/dag/index.mjs +0 -73
- package/dag/storage.mjs +0 -22
- package/federation/entity_key_chain.mjs +0 -9
- package/federation/topo_order_memo.mjs +0 -15
- package/{entity/files → files}/acl.mjs +3 -3
- package/files/assemble.mjs +55 -39
- package/files/assemble_stream.mjs +96 -44
- package/files/chunk_provider_registry.mjs +0 -6
- package/files/chunk_store.mjs +1 -11
- package/{entity/files → files}/evfs.mjs +81 -55
- package/{entity/files → files}/evfs_ref.mjs +3 -5
- package/files/manifest.mjs +11 -2
- package/{entity/files → files}/manifest_acl_registry.mjs +4 -10
- package/files/manifest_fetch.mjs +2 -2
- package/files/public_manifest.mjs +1 -1
- package/files/transfer_key.mjs +12 -6
- package/files/transfer_key_registry.mjs +0 -7
- package/governance/branch.mjs +0 -11
- package/link/channel_mux.mjs +2 -0
- package/mailbox/importance.mjs +0 -33
- package/mailbox/settings.mjs +0 -9
- package/node/denylist.mjs +0 -24
- package/node/identity.mjs +33 -2
- package/node/network.mjs +0 -51
- package/node/personal_block.mjs +1 -36
- package/node/reputation_store.mjs +0 -9
- package/node/retention_policy.mjs +0 -15
- package/package.json +1 -3
- package/registries/event_type.mjs +0 -12
- package/registries/inbound.mjs +0 -22
- package/reputation/engine.mjs +0 -11
- package/schemas/part_query.mjs +156 -0
- package/transport/ice_servers.mjs +0 -8
- package/transport/peer_identity_maps.mjs +0 -58
- package/transport/remote_user_room.mjs +0 -12
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/user_room.mjs +2 -11
- package/trust_graph/cache.mjs +0 -13
- package/trust_graph/engine.mjs +0 -24
- package/utils/async_mutex.mjs +0 -9
- package/wire/part_fanout.mjs +0 -19
- package/wire/part_query.mjs +487 -0
- package/wire/part_query.tunables.json +14 -0
- package/wire/part_query_cache.mjs +94 -0
- package/entity/files/replica_host_cache.mjs +0 -46
- package/entity/hosting_registry.mjs +0 -47
- package/entity/logical_entity_id_registry.mjs +0 -40
- package/entity/node_hash.mjs +0 -15
- package/entity/replica.mjs +0 -20
- package/entity/session_snapshot_registry.mjs +0 -38
- package/registries/p2p_viewer.mjs +0 -35
package/mailbox/importance.mjs
CHANGED
|
@@ -6,39 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
const TIER_ORDER = { quarantine: 0, normal: 1, trusted: 2 }
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* @param {number} score 信誉分
|
|
11
|
-
* @returns {MailboxTier} 分层
|
|
12
|
-
*/
|
|
13
|
-
export function mailboxTierFromScore(score) {
|
|
14
|
-
const numericScore = Number(score)
|
|
15
|
-
if (!Number.isFinite(numericScore)) return 'quarantine'
|
|
16
|
-
if (numericScore >= 0.45) return 'trusted'
|
|
17
|
-
if (numericScore >= 0.12) return 'normal'
|
|
18
|
-
return 'quarantine'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {object} opts 参数
|
|
23
|
-
* @param {number} [opts.senderScore] 发件方信誉
|
|
24
|
-
* @param {number} [opts.recipientScore] 收件方关系信誉(可选)
|
|
25
|
-
* @param {boolean} [opts.knownMember] 是否本地已知成员/节点
|
|
26
|
-
* @param {number} [opts.hop] 转发跳数
|
|
27
|
-
* @returns {{ tier: MailboxTier, score: number }} 分层与分数
|
|
28
|
-
*/
|
|
29
|
-
export function scoreMailboxImportance(opts = {}) {
|
|
30
|
-
const sender = Number(opts.senderScore ?? 0)
|
|
31
|
-
const recipient = Number(opts.recipientScore ?? sender)
|
|
32
|
-
const known = !!opts.knownMember
|
|
33
|
-
const hop = Math.max(0, Number(opts.hop) || 0)
|
|
34
|
-
let score = sender * 0.65 + recipient * 0.35
|
|
35
|
-
if (known) score += 0.15
|
|
36
|
-
score -= hop * 0.08
|
|
37
|
-
if (score < 0) score = 0
|
|
38
|
-
if (score > 1) score = 1
|
|
39
|
-
return { tier: mailboxTierFromScore(score), score }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
9
|
/**
|
|
43
10
|
* 按 tier 与 storedAt 排序(低 tier 先淘汰)。
|
|
44
11
|
* @param {object[]} rows 记录
|
package/mailbox/settings.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
2
1
|
import {
|
|
3
2
|
resolveMailboxRelayFanout,
|
|
4
3
|
resolveMailboxWantFanout,
|
|
@@ -43,11 +42,3 @@ export function resolveMailboxRoutingForPeerCount(peerCount, raw = {}, batterySa
|
|
|
43
42
|
batterySaver: true,
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @returns {{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }} mailbox 路由配置
|
|
49
|
-
*/
|
|
50
|
-
export function getMailboxRoutingSettings() {
|
|
51
|
-
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
52
|
-
return resolveMailboxRoutingForPeerCount(0, mailbox, batterySaver)
|
|
53
|
-
}
|
package/node/denylist.mjs
CHANGED
|
@@ -56,13 +56,6 @@ function buildDenylistIndex(blocked) {
|
|
|
56
56
|
return index
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
* @returns {void}
|
|
61
|
-
*/
|
|
62
|
-
export function invalidateDenylistIndex() {
|
|
63
|
-
cachedIndex = null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
59
|
/**
|
|
67
60
|
* @returns {DenylistIndex} 缓存索引
|
|
68
61
|
*/
|
|
@@ -244,23 +237,6 @@ export async function addDenylistFromBanContent(banContent, groupId) {
|
|
|
244
237
|
await addDenylistEntry({ scope: 'subject', value: pk, ...sourceGroupId ? { groupId: sourceGroupId } : {} })
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
/**
|
|
248
|
-
* @param {string} entityHash 128 位十六进制
|
|
249
|
-
* @param {boolean} block true=拉黑
|
|
250
|
-
* @returns {Promise<boolean>} 当前是否拉黑
|
|
251
|
-
*/
|
|
252
|
-
export async function setEntityBlocked(entityHash, block) {
|
|
253
|
-
const id = String(entityHash || '').trim().toLowerCase()
|
|
254
|
-
if (!isEntityHash128(id)) throw new Error('invalid entityHash')
|
|
255
|
-
await mutateDenylist(() => {
|
|
256
|
-
const list = loadDenylist()
|
|
257
|
-
const without = list.blocked.filter(e => !(e.scope === 'entity' && e.value === id))
|
|
258
|
-
if (block) without.push({ scope: 'entity', value: id })
|
|
259
|
-
saveDenylist({ blocked: without })
|
|
260
|
-
})
|
|
261
|
-
return block
|
|
262
|
-
}
|
|
263
|
-
|
|
264
240
|
/**
|
|
265
241
|
* 追加群 scope 拉黑项。
|
|
266
242
|
* @param {string} groupId 群 ID
|
package/node/identity.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
1
2
|
import { randomBytes } from 'node:crypto'
|
|
2
3
|
|
|
3
|
-
import { entityHashFromRecoveryPubKeyHex } from '../core/entity_id.mjs'
|
|
4
|
+
import { entityHashFromRecoveryPubKeyHex, parseEntityHash } from '../core/entity_id.mjs'
|
|
4
5
|
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
|
-
import {
|
|
6
|
+
import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
|
|
6
7
|
import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
|
|
7
8
|
|
|
8
9
|
import { emitNodeChange } from './instance.mjs'
|
|
@@ -11,6 +12,18 @@ import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
|
11
12
|
const NODE_SEED_HEX_RE = /^[\da-f]{64}$/iu
|
|
12
13
|
const NODE_JSON = 'node'
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 由持久化 nodeSeed 派生 nodeHash(64 hex)。
|
|
17
|
+
* @param {string} seedHex 32 字节 hex
|
|
18
|
+
* @returns {string} 节点哈希
|
|
19
|
+
*/
|
|
20
|
+
export function nodeHashFromSeed(seedHex) {
|
|
21
|
+
const seed = Buffer.from(String(seedHex).trim(), 'hex')
|
|
22
|
+
if (seed.length !== 32) throw new Error('invalid node seed')
|
|
23
|
+
const { publicKey } = keyPairFromSeed(seed)
|
|
24
|
+
return pubKeyHash(publicKey)
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
/**
|
|
15
28
|
* @returns {object} 节点配置磁盘对象
|
|
16
29
|
*/
|
|
@@ -97,3 +110,21 @@ export function entityHashFromKeys(nodeHash, recoveryPubKeyHex) {
|
|
|
97
110
|
if (!isHex64(nodeHash) || !isHex64(pub)) return null
|
|
98
111
|
return entityHashFromRecoveryPubKeyHex(nodeHash, pub)
|
|
99
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} recoveryPubKeyHex 64 位十六进制 recovery 公钥
|
|
116
|
+
* @returns {string | null} 本节点 entityHash
|
|
117
|
+
*/
|
|
118
|
+
export function resolveLocalEntityHashFromRecoveryPubKeyHex(recoveryPubKeyHex) {
|
|
119
|
+
return entityHashFromKeys(getNodeHash(), recoveryPubKeyHex)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} entityHash 目标 entityHash
|
|
124
|
+
* @returns {boolean} 是否为本节点可写实体
|
|
125
|
+
*/
|
|
126
|
+
export function isWritableLocalEntity(entityHash) {
|
|
127
|
+
const parsed = parseEntityHash(entityHash)
|
|
128
|
+
if (!parsed) return false
|
|
129
|
+
return parsed.nodeHash === getNodeHash()
|
|
130
|
+
}
|
package/node/network.mjs
CHANGED
|
@@ -121,20 +121,6 @@ export function saveNetwork(data) {
|
|
|
121
121
|
invalidateTrustGraphCache()
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* @param {string} nodeHash 64 位十六进制
|
|
126
|
-
* @param {'trusted' | 'explore'} tier 池档位
|
|
127
|
-
* @returns {void}
|
|
128
|
-
*/
|
|
129
|
-
export function addNetworkPeer(nodeHash, tier = 'explore') {
|
|
130
|
-
const id = normalizeHex64(nodeHash)
|
|
131
|
-
if (!isHex64(id)) return
|
|
132
|
-
const net = loadNetwork()
|
|
133
|
-
const list = tier === 'trusted' ? net.trustedPeers : net.explorePeers
|
|
134
|
-
if (!list.includes(id)) list.push(id)
|
|
135
|
-
saveNetwork(net)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
124
|
/**
|
|
139
125
|
* @param {{ nodeHash: string, source: string, kind: string, weight?: number, expiresAt?: number, ttlMs?: number, groupId?: string }} hint 扩边 hint
|
|
140
126
|
* @returns {void}
|
|
@@ -165,35 +151,6 @@ export function applyNetworkHint(hint) {
|
|
|
165
151
|
saveNetwork(net)
|
|
166
152
|
}
|
|
167
153
|
|
|
168
|
-
/**
|
|
169
|
-
* @param {{ remoteNodeHash?: string }[]} roster Trystero roster
|
|
170
|
-
* @param {string} [groupId] 来源群
|
|
171
|
-
* @param {string} [source='roster'] hint 来源标签
|
|
172
|
-
* @returns {void}
|
|
173
|
-
*/
|
|
174
|
-
export function recordExplorePeersFromRoster(roster, groupId = '', source = 'roster') {
|
|
175
|
-
if (!roster?.length) return
|
|
176
|
-
const net = loadNetwork()
|
|
177
|
-
const now = Date.now()
|
|
178
|
-
for (const peer of roster) {
|
|
179
|
-
const nodeHash = normalizeHex64(peer?.remoteNodeHash)
|
|
180
|
-
if (!isHex64(nodeHash)) continue
|
|
181
|
-
if (!net.explorePeers.includes(nodeHash))
|
|
182
|
-
net.explorePeers.push(nodeHash)
|
|
183
|
-
net.hints.push({
|
|
184
|
-
nodeHash,
|
|
185
|
-
source: String(source || 'roster'),
|
|
186
|
-
kind: 'roster',
|
|
187
|
-
weight: 0.1,
|
|
188
|
-
expiresAt: now + 24 * 60 * 60 * 1000,
|
|
189
|
-
...groupId ? { groupId: String(groupId).trim() } : {},
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
net.hints = capHintsBySource(net.hints).slice(-MAX_HINTS)
|
|
193
|
-
net.lastRosterAt = now
|
|
194
|
-
saveNetwork(net)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
154
|
/**
|
|
198
155
|
* 疑似分区/eclipse 后:用 trusted 锚点加宽 explore,便于恢复联邦可达。
|
|
199
156
|
* @returns {void}
|
|
@@ -239,14 +196,6 @@ export function mergeNetworkPeerPools(patch = {}) {
|
|
|
239
196
|
saveNetwork(net)
|
|
240
197
|
}
|
|
241
198
|
|
|
242
|
-
/**
|
|
243
|
-
* @param {string[]} nodeHashes trusted 候选
|
|
244
|
-
* @returns {void}
|
|
245
|
-
*/
|
|
246
|
-
export function mergeTrustedPeers(nodeHashes) {
|
|
247
|
-
mergeNetworkPeerPools({ trustedPeers: nodeHashes })
|
|
248
|
-
}
|
|
249
|
-
|
|
250
199
|
/**
|
|
251
200
|
* @param {string} groupId 群 scope
|
|
252
201
|
* @param {'node' | 'subject' | 'entity'} scope denylist 作用域
|
package/node/personal_block.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
7
7
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
8
|
-
import { isWritableLocalEntity } from '../entity/replica.mjs'
|
|
9
8
|
|
|
9
|
+
import { isWritableLocalEntity } from './identity.mjs'
|
|
10
10
|
import { isNodeInitialized, getEntityStore } from './instance.mjs'
|
|
11
11
|
|
|
12
12
|
/** @typedef {'subject' | 'entity'} PersonalListScope */
|
|
@@ -104,41 +104,6 @@ export async function loadPersonalBlockEntries(viewerEntityHash) {
|
|
|
104
104
|
return normalizePersonalListEntries(data?.blocked || [])
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
109
|
-
* @param {object} subject 待检主体
|
|
110
|
-
* @returns {Promise<boolean>} 是否被隐藏
|
|
111
|
-
*/
|
|
112
|
-
export async function isHiddenBy(viewerEntityHash, subject) {
|
|
113
|
-
return matchesPersonalListEntries(await loadPersonalHideEntries(viewerEntityHash), subject)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
118
|
-
* @param {object} subject 待检主体
|
|
119
|
-
* @returns {Promise<boolean>} 是否被拉黑
|
|
120
|
-
*/
|
|
121
|
-
export async function isBlockedBy(viewerEntityHash, subject) {
|
|
122
|
-
return matchesPersonalListEntries(await loadPersonalBlockEntries(viewerEntityHash), subject)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
127
|
-
* @param {object} subject 待检主体
|
|
128
|
-
* @returns {Promise<boolean>} 是否应过滤
|
|
129
|
-
*/
|
|
130
|
-
export async function isFilteredByPersonalLists(viewerEntityHash, subject) {
|
|
131
|
-
if (!viewerEntityHash) return false
|
|
132
|
-
const [blocked, hidden, muted] = await Promise.all([
|
|
133
|
-
loadPersonalBlockEntries(viewerEntityHash),
|
|
134
|
-
loadPersonalHideEntries(viewerEntityHash),
|
|
135
|
-
loadPersonalMuteEntries(viewerEntityHash),
|
|
136
|
-
])
|
|
137
|
-
return matchesPersonalListEntries(blocked, subject)
|
|
138
|
-
|| matchesPersonalListEntries(hidden, subject)
|
|
139
|
-
|| matchesPersonalListEntries(muted, subject)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
107
|
/**
|
|
143
108
|
* @param {string} viewerEntityHash 本地可写实体
|
|
144
109
|
* @param {string} targetEntityHash 目标
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
bumpChunkStorageReputationPure,
|
|
8
8
|
bumpReputationOnRelayPure,
|
|
9
9
|
ensureReputationShape,
|
|
10
|
-
isQuarantinedPure,
|
|
11
10
|
observeBehaviorSamplePure,
|
|
12
11
|
penalizeArchiveServeMismatchPure,
|
|
13
12
|
penalizeChunkStorageFailurePure,
|
|
@@ -145,14 +144,6 @@ export async function observePeerBehavior(peerNodeHash, sample) {
|
|
|
145
144
|
return anomaly
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
/**
|
|
149
|
-
* @param {string} peerNodeHash 对端
|
|
150
|
-
* @returns {boolean} 是否处于本地隔离
|
|
151
|
-
*/
|
|
152
|
-
export function isPeerQuarantined(peerNodeHash) {
|
|
153
|
-
return isQuarantinedPure(loadReputation(), peerNodeHash)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
147
|
/**
|
|
157
148
|
* @param {string} storagePeerKey 责任方
|
|
158
149
|
* @returns {void}
|
|
@@ -3,21 +3,6 @@ import {
|
|
|
3
3
|
authzFoldOrderIds,
|
|
4
4
|
descendantClosureFromTip,
|
|
5
5
|
} from '../governance/branch.mjs'
|
|
6
|
-
import { getPermissionAnchorTypes } from '../registries/event_type.mjs'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {string[]} order 拓扑序 id 列表
|
|
10
|
-
* @param {Map<string, object>} byId id → 事件
|
|
11
|
-
* @returns {number} 最早须保留的事件下标
|
|
12
|
-
*/
|
|
13
|
-
export function permissionAnchorStartIndex(order, byId) {
|
|
14
|
-
let anchor = order.length
|
|
15
|
-
for (let index = order.length - 1; index >= 0; index--) {
|
|
16
|
-
const ev = byId.get(order[index])
|
|
17
|
-
if (ev && getPermissionAnchorTypes().has(ev.type)) anchor = index
|
|
18
|
-
}
|
|
19
|
-
return anchor
|
|
20
|
-
}
|
|
21
6
|
|
|
22
7
|
/**
|
|
23
8
|
* 在共识分支上计算须保留的事件 id(连通子图,不用拓扑下标切片)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -45,8 +45,6 @@
|
|
|
45
45
|
"./rooms/*": "./rooms/*.mjs",
|
|
46
46
|
"./overlay": "./overlay/index.mjs",
|
|
47
47
|
"./overlay/*": "./overlay/*.mjs",
|
|
48
|
-
"./entity/*": "./entity/*.mjs",
|
|
49
|
-
"./entity/files/*": "./entity/files/*.mjs",
|
|
50
48
|
"./trust_graph/*": "./trust_graph/*.mjs",
|
|
51
49
|
"./mailbox/*": "./mailbox/*.mjs",
|
|
52
50
|
"./dag": "./dag/index.mjs",
|
|
@@ -56,18 +56,6 @@ export function getGovernanceAuthzTypes() {
|
|
|
56
56
|
return typesWithFlag('governance')
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/** 联邦入站/中继前须物化 ACL 门控的类型。 */
|
|
60
|
-
/** @returns {Set<string>} ACL 门控事件 type 集合 */
|
|
61
|
-
export function getFederationAclGatedEventTypes() {
|
|
62
|
-
return typesWithFlag('aclGated')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** §6.2 频道 GC 沉寂计时排除的类型。 */
|
|
66
|
-
/** @returns {Set<string>} GC 排除事件 type 集合 */
|
|
67
|
-
export function getChannelGcExcludedEventTypes() {
|
|
68
|
-
return typesWithFlag('gcExclude')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
59
|
/** 裁剪时不得早于最早一条权限锚点事件(§7.1)。 */
|
|
72
60
|
/** @returns {Set<string>} 权限锚点事件 type 集合 */
|
|
73
61
|
export function getPermissionAnchorTypes() {
|
package/registries/inbound.mjs
CHANGED
|
@@ -41,22 +41,6 @@ export function registerDeliveryInboundHandler(type, handler) {
|
|
|
41
41
|
deliveryHandlers.set(String(type || '').trim(), handler)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
* @param {string} type 入站类型
|
|
46
|
-
* @returns {void}
|
|
47
|
-
*/
|
|
48
|
-
export function unregisterRpcInboundHandler(type) {
|
|
49
|
-
rpcHandlers.delete(String(type || '').trim())
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {string} type 入站类型
|
|
54
|
-
* @returns {void}
|
|
55
|
-
*/
|
|
56
|
-
export function unregisterDeliveryInboundHandler(type) {
|
|
57
|
-
deliveryHandlers.delete(String(type || '').trim())
|
|
58
|
-
}
|
|
59
|
-
|
|
60
44
|
/**
|
|
61
45
|
* @param {InboundContext} ctx 入站上下文
|
|
62
46
|
* @param {object} message 已校验的线载荷(含 type)
|
|
@@ -82,9 +66,3 @@ export async function dispatchDeliveryInbound(ctx, message) {
|
|
|
82
66
|
if (!handler) return
|
|
83
67
|
await handler(ctx, message)
|
|
84
68
|
}
|
|
85
|
-
|
|
86
|
-
/** @returns {void} 测试用 */
|
|
87
|
-
export function clearInboundHandlers() {
|
|
88
|
-
rpcHandlers.clear()
|
|
89
|
-
deliveryHandlers.clear()
|
|
90
|
-
}
|
package/reputation/engine.mjs
CHANGED
|
@@ -445,14 +445,3 @@ export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
|
|
|
445
445
|
const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
|
|
446
446
|
return Number.isFinite(until) && until > now
|
|
447
447
|
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* @param {ReputationFile} data 信誉表
|
|
451
|
-
* @param {string} peerNodeHash 对端
|
|
452
|
-
* @param {number} [now] 当前时间
|
|
453
|
-
* @param {typeof reputationTunables} [tunables] tunables
|
|
454
|
-
* @returns {boolean} 是否检测到异常并触发隔离
|
|
455
|
-
*/
|
|
456
|
-
export function detectAnomalyPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
|
|
457
|
-
return isQuarantinedPure(data, peerNodeHash, now)
|
|
458
|
-
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
4
|
+
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { isPlainObject } from '../wire/ingress.mjs'
|
|
6
|
+
import { normalizePartpath } from '../wire/part_invoke.mjs'
|
|
7
|
+
import partQueryTunables from '../wire/part_query.tunables.json' with { type: 'json' }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* requestId: string
|
|
12
|
+
* originNodeHash: string
|
|
13
|
+
* partpath: string
|
|
14
|
+
* kind: string
|
|
15
|
+
* query: unknown
|
|
16
|
+
* ttl: number
|
|
17
|
+
* budget: { maxHits: number }
|
|
18
|
+
* }} PartQueryReq
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{
|
|
23
|
+
* requestId: string
|
|
24
|
+
* fromNodeHash: string
|
|
25
|
+
* rows: unknown[]
|
|
26
|
+
* }} PartQueryRes
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {unknown} value 任意 JSON
|
|
31
|
+
* @returns {number} UTF-8 序列化字节数;不可序列化时 Infinity
|
|
32
|
+
*/
|
|
33
|
+
export function measureJsonBytes(value) {
|
|
34
|
+
try {
|
|
35
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf8')
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return Number.POSITIVE_INFINITY
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {unknown} budget 请求预算
|
|
44
|
+
* @param {number} [maxHits=partQueryTunables.maxHits] 上限
|
|
45
|
+
* @returns {{ maxHits: number }} 钳制后的预算
|
|
46
|
+
*/
|
|
47
|
+
export function clampPartQueryBudget(budget, maxHits = partQueryTunables.maxHits) {
|
|
48
|
+
const cap = Math.max(1, Math.floor(Number(maxHits) || partQueryTunables.maxHits))
|
|
49
|
+
const raw = isPlainObject(budget) ? Number(budget.maxHits) : NaN
|
|
50
|
+
const hits = Number.isFinite(raw) ? Math.floor(raw) : cap
|
|
51
|
+
return { maxHits: Math.max(1, Math.min(cap, hits)) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} ttl 跳数预算
|
|
56
|
+
* @param {number} [maxTtl=partQueryTunables.maxTtl] 上限
|
|
57
|
+
* @returns {number | null} 钳制后的 ttl;非法为 null
|
|
58
|
+
*/
|
|
59
|
+
export function clampPartQueryTtl(ttl, maxTtl = partQueryTunables.maxTtl) {
|
|
60
|
+
const cap = Math.max(1, Math.floor(Number(maxTtl) || partQueryTunables.maxTtl))
|
|
61
|
+
const n = Math.floor(Number(ttl))
|
|
62
|
+
if (!Number.isFinite(n) || n < 1) return null
|
|
63
|
+
return Math.min(cap, n)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {unknown} rows 行数组
|
|
68
|
+
* @param {number} maxHits 条数上限
|
|
69
|
+
* @param {number} [maxRowsBytes=partQueryTunables.maxRowsBytes] 总尺寸上限
|
|
70
|
+
* @returns {unknown[] | null} 通过校验的 rows;失败 null
|
|
71
|
+
*/
|
|
72
|
+
export function clampPartQueryRows(rows, maxHits, maxRowsBytes = partQueryTunables.maxRowsBytes) {
|
|
73
|
+
if (!Array.isArray(rows)) return null
|
|
74
|
+
const limited = rows.slice(0, Math.max(0, Math.floor(Number(maxHits) || 0)))
|
|
75
|
+
if (measureJsonBytes(limited) > maxRowsBytes) return null
|
|
76
|
+
return limited
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** requestId 上限(randomUUID 36 字符;防超长键灌爆 dedupe/pending 表) */
|
|
80
|
+
const REQUEST_ID_MAX_LENGTH = 128
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {unknown} value 原始 requestId
|
|
84
|
+
* @returns {string | null} 修剪后的 requestId;非法 null
|
|
85
|
+
*/
|
|
86
|
+
function normalizeRequestId(value) {
|
|
87
|
+
const requestId = String(value || '').trim()
|
|
88
|
+
if (!requestId || requestId.length > REQUEST_ID_MAX_LENGTH) return null
|
|
89
|
+
return requestId
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} value 入站 req
|
|
94
|
+
* @param {typeof partQueryTunables} [tunables] 可调参数
|
|
95
|
+
* @returns {PartQueryReq | null} 校验通过的 req
|
|
96
|
+
*/
|
|
97
|
+
export function parsePartQueryReq(value, tunables = partQueryTunables) {
|
|
98
|
+
if (!isPlainObject(value)) return null
|
|
99
|
+
const requestId = normalizeRequestId(value.requestId)
|
|
100
|
+
if (!requestId) return null
|
|
101
|
+
const originNodeHash = normalizeHex64(value.originNodeHash)
|
|
102
|
+
if (!isHex64(originNodeHash)) return null
|
|
103
|
+
const partpath = normalizePartpath(value.partpath)
|
|
104
|
+
if (!partpath) return null
|
|
105
|
+
const kind = String(value.kind || '').trim()
|
|
106
|
+
if (!kind) return null
|
|
107
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'query')) return null
|
|
108
|
+
if (measureJsonBytes(value.query) > (tunables.maxQueryBytes ?? 2048)) return null
|
|
109
|
+
const ttl = clampPartQueryTtl(value.ttl, tunables.maxTtl)
|
|
110
|
+
if (ttl == null) return null
|
|
111
|
+
const budget = clampPartQueryBudget(value.budget, tunables.maxHits)
|
|
112
|
+
return {
|
|
113
|
+
requestId,
|
|
114
|
+
originNodeHash,
|
|
115
|
+
partpath,
|
|
116
|
+
kind,
|
|
117
|
+
query: value.query,
|
|
118
|
+
ttl,
|
|
119
|
+
budget,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {unknown} value 入站 res
|
|
125
|
+
* @param {typeof partQueryTunables} [tunables] 可调参数
|
|
126
|
+
* @returns {PartQueryRes | null} 校验通过的 res
|
|
127
|
+
*/
|
|
128
|
+
export function parsePartQueryRes(value, tunables = partQueryTunables) {
|
|
129
|
+
if (!isPlainObject(value)) return null
|
|
130
|
+
const requestId = normalizeRequestId(value.requestId)
|
|
131
|
+
if (!requestId) return null
|
|
132
|
+
const fromNodeHash = normalizeHex64(value.fromNodeHash)
|
|
133
|
+
if (!isHex64(fromNodeHash)) return null
|
|
134
|
+
const rows = clampPartQueryRows(value.rows, tunables.maxHits, tunables.maxRowsBytes)
|
|
135
|
+
if (!rows) return null
|
|
136
|
+
return { requestId, fromNodeHash, rows }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {unknown} partpath part 路径
|
|
141
|
+
* @param {unknown} kind 查询标签
|
|
142
|
+
* @param {unknown} query 不透明查询
|
|
143
|
+
* @returns {string | null} 规范化三元组的缓存材料;非法 null
|
|
144
|
+
*/
|
|
145
|
+
export function normalizePartQueryCacheMaterial(partpath, kind, query) {
|
|
146
|
+
const path = normalizePartpath(partpath)
|
|
147
|
+
const k = String(kind || '').trim()
|
|
148
|
+
if (!path || !k) return null
|
|
149
|
+
if (measureJsonBytes(query) > partQueryTunables.maxQueryBytes) return null
|
|
150
|
+
try {
|
|
151
|
+
return canonicalStringify({ partpath: path, kind: k, query })
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -47,14 +47,6 @@ export function resolveIceServers(groupSettings) {
|
|
|
47
47
|
return out.length ? out : [...DEFAULT_ICE_SERVERS]
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {unknown} groupSettings 物化群设置
|
|
52
|
-
* @returns {{ iceServers: ReturnType<typeof resolveIceServers> }} Trystero rtcConfig 片段
|
|
53
|
-
*/
|
|
54
|
-
export function resolveIceServersForTrystero(groupSettings) {
|
|
55
|
-
return { iceServers: resolveIceServers(groupSettings) }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
50
|
/**
|
|
59
51
|
* 校验并规范化待写入 DAG 的 iceServers 数组。
|
|
60
52
|
* @param {unknown} raw 请求体字段
|
|
@@ -34,61 +34,3 @@ export function pruneStaleRosterEntries(peerToNode, nodeToPeer, livePeerIds) {
|
|
|
34
34
|
}
|
|
35
35
|
return stale
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 创建 peer id 与 nodeHash 双向映射及 roster 查询辅助。
|
|
40
|
-
* @param {object} [opts] 选项
|
|
41
|
-
* @param {() => Iterable<string>} [opts.getLivePeerIds] 获取当前在线 peer id 的回调
|
|
42
|
-
* @param {(stale: Array<{ peerId: string, remoteNodeHash?: string }>) => void} [opts.onStalePruned] stale 条目被清理时的回调
|
|
43
|
-
* @returns {{ peerToNode: Map<string, string>, nodeToPeer: Map<string, string>, getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>, getPeerIdByNodeHash: (nodeHash: string) => string | null, onPeerLeave: (peerId: string) => void }} 映射与查询接口
|
|
44
|
-
*/
|
|
45
|
-
export function createPeerIdentityMaps(opts = {}) {
|
|
46
|
-
/** @type {Map<string, string>} */
|
|
47
|
-
const peerToNode = new Map()
|
|
48
|
-
/** @type {Map<string, string>} */
|
|
49
|
-
const nodeToPeer = new Map()
|
|
50
|
-
const getLivePeerIds = typeof opts.getLivePeerIds === 'function' ? opts.getLivePeerIds : null
|
|
51
|
-
const onStalePruned = typeof opts.onStalePruned === 'function' ? opts.onStalePruned : null
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 根据 live peer 集合清理 stale 映射。
|
|
55
|
-
* @returns {void}
|
|
56
|
-
*/
|
|
57
|
-
const reconcile = () => {
|
|
58
|
-
if (!getLivePeerIds) return
|
|
59
|
-
const stale = pruneStaleRosterEntries(peerToNode, nodeToPeer, getLivePeerIds())
|
|
60
|
-
if (stale.length && onStalePruned) onStalePruned(stale)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
peerToNode,
|
|
65
|
-
nodeToPeer,
|
|
66
|
-
/**
|
|
67
|
-
* 返回当前 roster(会先 reconcile stale 条目)。
|
|
68
|
-
* @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} roster 列表
|
|
69
|
-
*/
|
|
70
|
-
getRoster() {
|
|
71
|
-
reconcile()
|
|
72
|
-
return [...peerToNode.entries()].map(([peerId, remoteNodeHash]) => ({ peerId, remoteNodeHash }))
|
|
73
|
-
},
|
|
74
|
-
/**
|
|
75
|
-
* 按 nodeHash 查找对应 peer id。
|
|
76
|
-
* @param {string} targetNodeHash 目标节点 64 hex
|
|
77
|
-
* @returns {string | null} 对端 id;无映射时为 null
|
|
78
|
-
*/
|
|
79
|
-
getPeerIdByNodeHash(targetNodeHash) {
|
|
80
|
-
reconcile()
|
|
81
|
-
return nodeToPeer.get(String(targetNodeHash).trim().toLowerCase()) || null
|
|
82
|
-
},
|
|
83
|
-
/**
|
|
84
|
-
* peer 离线时从映射中移除。
|
|
85
|
-
* @param {string} peerId 离线的 peer id
|
|
86
|
-
* @returns {void}
|
|
87
|
-
*/
|
|
88
|
-
onPeerLeave(peerId) {
|
|
89
|
-
const remote = peerToNode.get(peerId)
|
|
90
|
-
if (remote) nodeToPeer.delete(remote)
|
|
91
|
-
peerToNode.delete(peerId)
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -95,15 +95,3 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
95
95
|
inflights.set(key, task)
|
|
96
96
|
return await task
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 释放目标节点的远端用户房间连接。
|
|
101
|
-
* @param {string} targetNodeHash 目标节点 64 hex
|
|
102
|
-
* @returns {void}
|
|
103
|
-
*/
|
|
104
|
-
export function releaseRemoteUserRoom(targetNodeHash) {
|
|
105
|
-
const key = targetNodeHash.toLowerCase()
|
|
106
|
-
const slot = slots.get(key)
|
|
107
|
-
if (slot) void Promise.resolve(slot.leave()).catch(() => { })
|
|
108
|
-
slots.delete(key)
|
|
109
|
-
}
|