@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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/core/bytes_codec.mjs +0 -27
  3. package/core/constants.mjs +0 -31
  4. package/core/hexIds.mjs +0 -13
  5. package/{entity → core}/logical_entity.mjs +2 -1
  6. package/crypto/key.mjs +14 -15
  7. package/dag/index.mjs +0 -73
  8. package/dag/storage.mjs +0 -22
  9. package/federation/entity_key_chain.mjs +0 -9
  10. package/federation/topo_order_memo.mjs +0 -15
  11. package/{entity/files → files}/acl.mjs +3 -3
  12. package/files/assemble.mjs +55 -39
  13. package/files/assemble_stream.mjs +96 -44
  14. package/files/chunk_provider_registry.mjs +0 -6
  15. package/files/chunk_store.mjs +1 -11
  16. package/{entity/files → files}/evfs.mjs +81 -55
  17. package/{entity/files → files}/evfs_ref.mjs +3 -5
  18. package/files/manifest.mjs +11 -2
  19. package/{entity/files → files}/manifest_acl_registry.mjs +4 -10
  20. package/files/manifest_fetch.mjs +2 -2
  21. package/files/public_manifest.mjs +1 -1
  22. package/files/transfer_key.mjs +12 -6
  23. package/files/transfer_key_registry.mjs +0 -7
  24. package/governance/branch.mjs +0 -11
  25. package/link/channel_mux.mjs +2 -0
  26. package/mailbox/importance.mjs +0 -33
  27. package/mailbox/settings.mjs +0 -9
  28. package/node/denylist.mjs +0 -24
  29. package/node/identity.mjs +33 -2
  30. package/node/network.mjs +0 -51
  31. package/node/personal_block.mjs +1 -36
  32. package/node/reputation_store.mjs +0 -9
  33. package/node/retention_policy.mjs +0 -15
  34. package/package.json +1 -3
  35. package/registries/event_type.mjs +0 -12
  36. package/registries/inbound.mjs +0 -22
  37. package/reputation/engine.mjs +0 -11
  38. package/schemas/part_query.mjs +156 -0
  39. package/transport/ice_servers.mjs +0 -8
  40. package/transport/peer_identity_maps.mjs +0 -58
  41. package/transport/remote_user_room.mjs +0 -12
  42. package/transport/rtc_connection_budget.mjs +2 -0
  43. package/transport/user_room.mjs +2 -11
  44. package/trust_graph/cache.mjs +0 -13
  45. package/trust_graph/engine.mjs +0 -24
  46. package/utils/async_mutex.mjs +0 -9
  47. package/wire/part_fanout.mjs +0 -19
  48. package/wire/part_query.mjs +487 -0
  49. package/wire/part_query.tunables.json +14 -0
  50. package/wire/part_query_cache.mjs +94 -0
  51. package/entity/files/replica_host_cache.mjs +0 -46
  52. package/entity/hosting_registry.mjs +0 -47
  53. package/entity/logical_entity_id_registry.mjs +0 -40
  54. package/entity/node_hash.mjs +0 -15
  55. package/entity/replica.mjs +0 -20
  56. package/entity/session_snapshot_registry.mjs +0 -38
  57. package/registries/p2p_viewer.mjs +0 -35
@@ -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 记录
@@ -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 { nodeHashFromSeed } from '../entity/node_hash.mjs'
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 作用域
@@ -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",
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() {
@@ -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
- }
@@ -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
- }
@@ -162,6 +162,8 @@ const NON_CRITICAL_FED_ACTIONS = new Set([
162
162
  'fed_manifest_get',
163
163
  'fed_manifest_data',
164
164
  'part_invoke',
165
+ 'part_query_req',
166
+ 'part_query_res',
165
167
  'discovery_announce',
166
168
  'discovery_query',
167
169
  'char_rpc',