@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.
Files changed (100) hide show
  1. package/README.md +3 -2
  2. package/core/bytes_codec.mjs +5 -6
  3. package/core/composite_key.mjs +0 -79
  4. package/core/entity_id_parse.mjs +2 -3
  5. package/core/evfs_logical_path.mjs +2 -3
  6. package/core/hexIds.mjs +5 -5
  7. package/core/logical_entity.mjs +3 -4
  8. package/crypto/channel.mjs +2 -25
  9. package/crypto/checkpoint_sign.mjs +5 -4
  10. package/crypto/crypto.mjs +0 -8
  11. package/crypto/key.mjs +9 -10
  12. package/dag/canonicalize_row.mjs +1 -1
  13. package/discovery/advert_peer_hints.mjs +1 -1
  14. package/discovery/adverts.mjs +7 -12
  15. package/discovery/bt/index.mjs +6 -11
  16. package/discovery/bt/peer_hints.mjs +2 -3
  17. package/discovery/bt/runtime.mjs +1 -1
  18. package/discovery/index.mjs +14 -14
  19. package/discovery/lan.mjs +3 -3
  20. package/discovery/lan_interfaces.mjs +2 -2
  21. package/discovery/lan_peer_hints.mjs +1 -1
  22. package/discovery/nostr.mjs +19 -24
  23. package/federation/entity_key_chain.mjs +5 -24
  24. package/federation/message_rate_limit.mjs +3 -2
  25. package/federation/part_query/runtime.mjs +9 -20
  26. package/files/assemble.mjs +3 -3
  27. package/files/assemble_stream.mjs +1 -1
  28. package/files/chunk/fetch.mjs +1 -2
  29. package/files/chunk/responder.mjs +4 -41
  30. package/files/chunk/store.mjs +2 -3
  31. package/files/chunk/verify.mjs +2 -3
  32. package/files/evfs.mjs +1 -1
  33. package/files/evfs_ref.mjs +3 -4
  34. package/files/fetch_fanout.mjs +3 -3
  35. package/files/manifest/fetch.mjs +2 -3
  36. package/files/manifest/normalize.mjs +9 -9
  37. package/files/manifest/pending.mjs +1 -4
  38. package/files/manifest/public.mjs +8 -8
  39. package/files/transfer_key.mjs +3 -3
  40. package/files/transfer_key_registry.mjs +6 -6
  41. package/governance/branch.mjs +2 -1
  42. package/governance/join_pow.mjs +7 -5
  43. package/governance/owner_succession_ballot.mjs +6 -6
  44. package/index.mjs +8 -2
  45. package/infra/default_node_dir.mjs +2 -2
  46. package/infra/priority.mjs +2 -2
  47. package/link/frame.mjs +2 -3
  48. package/link/handshake.mjs +6 -6
  49. package/link/pipe.mjs +69 -5
  50. package/link/providers/ble_gatt.mjs +1 -9
  51. package/link/providers/index.mjs +2 -2
  52. package/link/providers/nostr.mjs +1 -3
  53. package/link/providers/webrtc.mjs +2 -2
  54. package/link/rtc/ice_local_hostname.mjs +6 -5
  55. package/link/rtc/w3c_bridge.mjs +1 -1
  56. package/link/sdp_fingerprint.mjs +1 -1
  57. package/mailbox/consumer_registry.mjs +2 -2
  58. package/mailbox/deliver_or_store.mjs +4 -4
  59. package/mailbox/parse.mjs +1 -1
  60. package/mailbox/prune.mjs +2 -2
  61. package/mailbox/rate.mjs +3 -10
  62. package/mailbox/store.mjs +7 -7
  63. package/node/denylist.mjs +38 -38
  64. package/node/entity_store.mjs +5 -5
  65. package/node/identity.mjs +7 -8
  66. package/node/instance.mjs +1 -1
  67. package/node/log.mjs +1 -1
  68. package/node/network.mjs +14 -16
  69. package/node/personal_block.mjs +22 -20
  70. package/node/reputation_store.mjs +11 -19
  71. package/node/reputation_sync.mjs +0 -7
  72. package/node/signaling_config.mjs +1 -1
  73. package/node/storage_plugins.mjs +2 -2
  74. package/overlay/index.mjs +1 -1
  75. package/package.json +1 -1
  76. package/registries/event_type.mjs +2 -2
  77. package/registries/room_provider.mjs +2 -2
  78. package/reputation/engine.mjs +17 -15
  79. package/schemas/discovery.mjs +5 -5
  80. package/schemas/federation_pull.mjs +16 -24
  81. package/schemas/part_query.mjs +4 -4
  82. package/timeline/verify_remote.mjs +4 -4
  83. package/transport/group_link_set.mjs +3 -3
  84. package/transport/ice_servers.mjs +2 -2
  85. package/transport/link_registry.mjs +51 -20
  86. package/transport/mesh_keepalive.mjs +1 -1
  87. package/transport/node_scope/wire.mjs +1 -1
  88. package/transport/peer_health.mjs +127 -0
  89. package/transport/remote_user_room.mjs +13 -15
  90. package/transport/rtc_connection_budget.mjs +6 -6
  91. package/transport/runtime_bootstrap.mjs +12 -12
  92. package/trust_graph/cache.mjs +2 -3
  93. package/trust_graph/engine.mjs +1 -1
  94. package/trust_graph/registry.mjs +2 -2
  95. package/trust_graph/send.mjs +1 -1
  96. package/utils/async_mutex.mjs +1 -1
  97. package/wire/ingress.mjs +1 -1
  98. package/wire/part/ingress.mjs +6 -6
  99. package/wire/part/query.mjs +3 -3
  100. package/wire/volatile_signature.mjs +4 -9
package/mailbox/store.mjs CHANGED
@@ -73,7 +73,7 @@ export function mailboxTierFromHop(hop) {
73
73
  */
74
74
  export function isDeliverableMailboxRecord(row) {
75
75
  if (!row?.envelope || typeof row.envelope !== 'object') return false
76
- if (!String(row.app || '').trim()) return false
76
+ if (!String(row.app || '')) return false
77
77
  return row.tier === 'trusted' || row.tier === 'normal'
78
78
  }
79
79
 
@@ -139,7 +139,7 @@ async function writeAll(rows) {
139
139
  * @returns {string} 稳定信封 id
140
140
  */
141
141
  export function mailboxEnvelopeId(envelope) {
142
- const id = String(envelope?.id || '').trim().toLowerCase()
142
+ const id = String(envelope?.id || '')
143
143
  if (!id) throw new Error('mailbox envelope id required')
144
144
  return id
145
145
  }
@@ -155,7 +155,7 @@ export async function storeMailboxRecord(record) {
155
155
  const hop = normalizeMailboxHop(record.hop)
156
156
  const { tier } = record
157
157
  if (!['trusted', 'normal', 'quarantine'].includes(tier)) return false
158
- const app = String(record.app || '').trim()
158
+ const app = String(record.app || '')
159
159
  if (!app) return false
160
160
  let id
161
161
  try {
@@ -172,13 +172,13 @@ export async function storeMailboxRecord(record) {
172
172
  id,
173
173
  app,
174
174
  toPubKeyHash,
175
- dmSessionTag: record.dmSessionTag?.trim().toLowerCase() || undefined,
175
+ dmSessionTag: record.dmSessionTag ? String(record.dmSessionTag) : undefined,
176
176
  groupId: record.groupId || undefined,
177
177
  channelId: record.channelId || undefined,
178
178
  envelope: record.envelope,
179
179
  storedAt: Date.now(),
180
180
  expiresAt: Date.now() + ttlMs,
181
- fromNodeHash: String(record.fromNodeHash || '').trim(),
181
+ fromNodeHash: normalizeHex64(record.fromNodeHash),
182
182
  hop,
183
183
  tier,
184
184
  importance: Number.isFinite(Number(record.importance)) ? Number(record.importance) : undefined,
@@ -204,7 +204,7 @@ export async function listMailboxIdsForRecipient(toPubKeyHash) {
204
204
  * @returns {Promise<MailboxRecord[]>} 匹配记录
205
205
  */
206
206
  export async function getMailboxRecords(ids) {
207
- const want = new Set(ids.map(id => String(id).trim().toLowerCase()))
207
+ const want = new Set(ids.map(id => String(id)))
208
208
  return (await readAll()).filter(record => want.has(record.id))
209
209
  }
210
210
 
@@ -213,7 +213,7 @@ export async function getMailboxRecords(ids) {
213
213
  * @returns {Promise<void>}
214
214
  */
215
215
  export async function deleteMailboxRecords(ids) {
216
- const drop = new Set(ids.map(id => String(id).trim().toLowerCase()))
216
+ const drop = new Set(ids.map(id => String(id)))
217
217
  return withAsyncMutex(mailboxStoreMutexKey(), async () => {
218
218
  await writeAll((await readAll()).filter(record => !drop.has(record.id)))
219
219
  })
package/node/denylist.mjs CHANGED
@@ -46,7 +46,7 @@ function buildDenylistIndex(blocked) {
46
46
  /** @type {DenylistIndex} */
47
47
  const index = { blocked, keys: new Set() }
48
48
  for (const entry of blocked) {
49
- const gid = String(entry.groupId || '').trim() || '*'
49
+ const gid = entry.groupId || '*'
50
50
  if (entry.scope === 'entity') {
51
51
  index.keys.add(denyKey('entity', '*', entry.value))
52
52
  continue
@@ -75,19 +75,20 @@ export function normalizeDenylist(raw) {
75
75
  /** @type {Array<{ scope: DenyScope, value: string, groupId?: string }>} */
76
76
  const blocked = []
77
77
  for (const entry of raw?.blocked || []) {
78
- const scope = String(entry?.scope || '').trim().toLowerCase()
79
- const value = String(entry?.value || '').trim().toLowerCase()
80
- const groupId = String(entry.groupId || '').trim()
81
- if (!scope || !value) continue
78
+ const scope = String(entry?.scope || '')
79
+ const groupId = String(entry.groupId || '')
80
+ if (!scope) continue
82
81
  if (scope === 'entity') {
82
+ const value = String(entry?.value || '')
83
83
  if (isEntityHash128(value))
84
84
  blocked.push({ scope: 'entity', value })
85
85
  continue
86
86
  }
87
- if (scope === 'node' && isHex64(normalizeHex64(value)))
88
- blocked.push({ scope: 'node', value: normalizeHex64(value), ...groupId ? { groupId } : {} })
89
- else if (scope === 'subject' && isHex64(normalizeHex64(value)))
90
- blocked.push({ scope: 'subject', value: normalizeHex64(value), ...groupId ? { groupId } : {} })
87
+ const value = normalizeHex64(entry?.value)
88
+ if (scope === 'node' && isHex64(value))
89
+ blocked.push({ scope: 'node', value, ...groupId ? { groupId } : {} })
90
+ else if (scope === 'subject' && isHex64(value))
91
+ blocked.push({ scope: 'subject', value, ...groupId ? { groupId } : {} })
91
92
  }
92
93
  return { blocked }
93
94
  }
@@ -117,7 +118,7 @@ export function saveDenylist(list) {
117
118
  export function isSubjectBannedByState(state, subject) {
118
119
  const pk = normalizeHex64(subject?.pubKeyHash)
119
120
  if (isHex64(pk) && state?.bannedMembers?.has?.(pk)) return true
120
- const entity = String(subject?.entityHash || '').trim().toLowerCase()
121
+ const entity = String(subject?.entityHash || '')
121
122
  if (isEntityHash128(entity) && state?.bannedEntities?.has?.(entity)) return true
122
123
  const node = normalizeHex64(subject?.nodeHash)
123
124
  if (isHex64(node) && state?.bannedNodes?.has?.(node)) return true
@@ -132,17 +133,16 @@ export function isSubjectBannedByState(state, subject) {
132
133
  */
133
134
  function matchesDenylistIndex(index, subject, groupId = '') {
134
135
  const pk = normalizeHex64(subject?.pubKeyHash)
135
- const entity = String(subject?.entityHash || '').trim().toLowerCase()
136
+ const entity = String(subject?.entityHash || '')
136
137
  const node = normalizeHex64(subject?.nodeHash)
137
- const gid = String(groupId || '').trim()
138
138
  const { keys } = index
139
139
 
140
140
  if (entity && keys.has(denyKey('entity', '*', entity))) return true
141
141
  if (isHex64(node) && keys.has(denyKey('node', '*', node))) return true
142
142
  if (isHex64(pk) && keys.has(denyKey('subject', '*', pk))) return true
143
- if (!gid) return false
144
- if (isHex64(pk) && keys.has(denyKey('subject', gid, pk))) return true
145
- if (isHex64(node) && keys.has(denyKey('node', gid, node))) return true
143
+ if (!groupId) return false
144
+ if (isHex64(pk) && keys.has(denyKey('subject', groupId, pk))) return true
145
+ if (isHex64(node) && keys.has(denyKey('node', groupId, node))) return true
146
146
  return false
147
147
  }
148
148
 
@@ -164,13 +164,12 @@ export function isPeerKeyBlocked(groupId, peerKey) {
164
164
  const key = normalizeHex64(peerKey)
165
165
  if (!isHex64(key)) return false
166
166
  const index = getDenylistIndex()
167
- const gid = String(groupId || '').trim()
168
167
  const { keys } = index
169
168
  if (keys.has(denyKey('subject', '*', key))) return true
170
169
  if (keys.has(denyKey('node', '*', key))) return true
171
- if (!gid) return false
172
- if (keys.has(denyKey('subject', gid, key))) return true
173
- if (keys.has(denyKey('node', gid, key))) return true
170
+ if (!groupId) return false
171
+ if (keys.has(denyKey('subject', groupId, key))) return true
172
+ if (keys.has(denyKey('node', groupId, key))) return true
174
173
  return false
175
174
  }
176
175
 
@@ -196,21 +195,24 @@ export function isEntityHashBlocked(entityHash) {
196
195
  * @returns {Promise<void>}
197
196
  */
198
197
  export function addDenylistEntry(entry) {
199
- const scope = String(entry?.scope || '').trim().toLowerCase()
200
- const value = String(entry?.value || '').trim().toLowerCase()
201
- if (!scope || !value)
198
+ const { scope } = entry
199
+ if (!scope)
202
200
  throw new Error('scope and value required')
203
201
  if (scope === 'entity' && entry.groupId)
204
202
  throw new Error('entity scope does not use groupId')
205
- if (scope === 'subject' && !isHex64(normalizeHex64(value)))
203
+ const normValue = scope === 'node' || scope === 'subject'
204
+ ? normalizeHex64(entry.value)
205
+ : entry.value
206
+ if (!normValue)
207
+ throw new Error('scope and value required')
208
+ if (scope === 'subject' && !isHex64(normValue))
206
209
  throw new Error('invalid pubKeyHash')
207
- if (scope === 'entity' && !isEntityHash128(value))
210
+ if (scope === 'entity' && !isEntityHash128(normValue))
208
211
  throw new Error('invalid entityHash')
209
- if (scope === 'node' && !isHex64(normalizeHex64(value)))
212
+ if (scope === 'node' && !isHex64(normValue))
210
213
  throw new Error('invalid nodeHash')
211
214
 
212
- const normValue = scope === 'node' || scope === 'subject' ? normalizeHex64(value) : value
213
- const groupId = entry.groupId ? String(entry.groupId).trim() : undefined
215
+ const groupId = entry.groupId || undefined
214
216
  return mutateDenylist(() => {
215
217
  const list = loadDenylist()
216
218
  if (list.blocked.some(row => row.scope === scope && row.value === normValue && row.groupId === groupId))
@@ -226,8 +228,8 @@ export function addDenylistEntry(entry) {
226
228
  * @returns {Promise<void>}
227
229
  */
228
230
  export async function addDenylistFromBanContent(banContent, groupId) {
229
- const scope = String(banContent?.banScope || 'entity').trim().toLowerCase()
230
- const sourceGroupId = String(groupId || '').trim()
231
+ const scope = String(banContent?.banScope || 'entity')
232
+ const sourceGroupId = groupId || ''
231
233
  if (scope === 'entity' && banContent?.targetEntityHash)
232
234
  await addDenylistEntry({ scope: 'entity', value: banContent.targetEntityHash })
233
235
  if (scope === 'node' && banContent?.targetNodeHash)
@@ -245,10 +247,9 @@ export async function addDenylistFromBanContent(banContent, groupId) {
245
247
  * @returns {Promise<void>}
246
248
  */
247
249
  export function addGroupBlockedPeer(groupId, scope, value) {
248
- const normScope = String(scope || '').trim().toLowerCase()
249
- if (normScope === 'entity')
250
- return addDenylistEntry({ scope: normScope, value })
251
- return addDenylistEntry({ scope: normScope, value, groupId })
250
+ if (scope === 'entity')
251
+ return addDenylistEntry({ scope, value })
252
+ return addDenylistEntry({ scope, value, groupId })
252
253
  }
253
254
 
254
255
  /**
@@ -258,14 +259,13 @@ export function addGroupBlockedPeer(groupId, scope, value) {
258
259
  * @returns {Promise<void>}
259
260
  */
260
261
  export function removeGroupBlockedPeer(groupId, scope, value) {
261
- const normScope = String(scope || '').trim().toLowerCase()
262
- const id = String(value || '').trim().toLowerCase()
263
- if (!normScope || !id) return Promise.resolve()
262
+ const id = scope === 'entity' ? value : normalizeHex64(value)
263
+ if (!scope || !id) return Promise.resolve()
264
264
  return mutateDenylist(() => {
265
265
  const list = loadDenylist()
266
266
  list.blocked = list.blocked.filter(entry => {
267
- if (entry.scope !== normScope || entry.value !== id) return true
268
- if (normScope === 'entity') return false
267
+ if (entry.scope !== scope || entry.value !== id) return true
268
+ if (scope === 'entity') return false
269
269
  return entry.groupId !== groupId
270
270
  })
271
271
  saveDenylist(list)
@@ -25,9 +25,9 @@ import { readJsonFile, writeJsonFile } from '../utils/json_io.mjs'
25
25
  * @returns {string} 规范化 entityHash
26
26
  */
27
27
  function normalizeEntityHash(entityHash) {
28
- const hash = String(entityHash || '').trim().toLowerCase()
29
- if (!parseEntityHash(hash)) throw new Error('invalid entityHash')
30
- return hash
28
+ const parsed = parseEntityHash(entityHash)
29
+ if (!parsed) throw new Error('invalid entityHash')
30
+ return parsed.entityHash
31
31
  }
32
32
 
33
33
  /**
@@ -51,7 +51,7 @@ export function createFsEntityStore(baseDir) {
51
51
  * @returns {string} JSON 文件绝对路径
52
52
  */
53
53
  function entityJsonPath(entityHash, name) {
54
- const safe = String(name || '').trim().replace(/\\/g, '/')
54
+ const safe = name.replace(/\\/g, '/')
55
55
  if (!safe || safe.includes('..') || safe.startsWith('/')) throw new Error('invalid entity json name')
56
56
  return path.join(entityRoot(entityHash), safe)
57
57
  }
@@ -78,7 +78,7 @@ export function createFsEntityStore(baseDir) {
78
78
  async listEntityHashes() {
79
79
  try {
80
80
  const entries = await fsp.readdir(root, { withFileTypes: true })
81
- return entries.filter(e => e.isDirectory()).map(e => e.name.toLowerCase()).filter(h => parseEntityHash(h))
81
+ return entries.filter(e => e.isDirectory()).map(e => e.name).filter(h => parseEntityHash(h))
82
82
  }
83
83
  catch (error) {
84
84
  if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return []
package/node/identity.mjs CHANGED
@@ -2,14 +2,13 @@ import { Buffer } from 'node:buffer'
2
2
  import { randomBytes } from 'node:crypto'
3
3
 
4
4
  import { entityHashFromRecoveryPubKeyHex, parseEntityHash } from '../core/entity_id.mjs'
5
- import { isHex64 } from '../core/hexIds.mjs'
5
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
6
6
  import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
7
7
  import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
8
8
 
9
9
  import { emitNodeChange } from './instance.mjs'
10
10
  import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
11
11
 
12
- const NODE_SEED_HEX_RE = /^[\da-f]{64}$/iu
13
12
  const NODE_JSON = 'node'
14
13
 
15
14
  /**
@@ -18,7 +17,7 @@ const NODE_JSON = 'node'
18
17
  * @returns {string} 节点哈希
19
18
  */
20
19
  export function nodeHashFromSeed(seedHex) {
21
- const seed = Buffer.from(String(seedHex).trim(), 'hex')
20
+ const seed = Buffer.from(normalizeHex64(seedHex), 'hex')
22
21
  if (seed.length !== 32) throw new Error('invalid node seed')
23
22
  const { publicKey } = keyPairFromSeed(seed)
24
23
  return pubKeyHash(publicKey)
@@ -47,8 +46,8 @@ function saveNodeFile(patch) {
47
46
  */
48
47
  export function ensureNodeSeed() {
49
48
  const data = loadNodeFile()
50
- const existing = String(data.nodeSeedHex || '').trim().toLowerCase()
51
- if (NODE_SEED_HEX_RE.test(existing)) return existing
49
+ const existing = normalizeHex64(data.nodeSeedHex)
50
+ if (isHex64(existing)) return existing
52
51
  const nodeSeedHex = randomBytes(32).toString('hex')
53
52
  saveNodeFile({ nodeSeedHex })
54
53
  return nodeSeedHex
@@ -67,7 +66,7 @@ export function getNodeHash() {
67
66
  export function getNodeTransportSettings() {
68
67
  const data = loadNodeFile()
69
68
  const relayUrls = (data.relayUrls || [])
70
- .map(url => url.trim())
69
+ .map(url => String(url || ''))
71
70
  .filter(url => url.startsWith('wss://'))
72
71
  const batterySaver = !!data.batterySaver
73
72
  const mailbox = normalizeMailboxSettings(data.mailbox || {})
@@ -82,7 +81,7 @@ export function saveNodeTransportSettings(patch) {
82
81
  const data = loadNodeFile()
83
82
  if (patch.batterySaver != null) data.batterySaver = !!patch.batterySaver
84
83
  if (patch.relayUrls)
85
- data.relayUrls = patch.relayUrls.map(url => url.trim()).filter(url => url.startsWith('wss://'))
84
+ data.relayUrls = patch.relayUrls.map(url => String(url || '')).filter(url => url.startsWith('wss://'))
86
85
  if (patch.mailbox)
87
86
  data.mailbox = normalizeMailboxSettings({ ...data.mailbox, ...patch.mailbox })
88
87
  saveNodeFile(data)
@@ -106,7 +105,7 @@ export function ensureNodeDefaults() {
106
105
  * @returns {string | null} entityHash(非法 hex 时 null)
107
106
  */
108
107
  export function entityHashFromKeys(nodeHash, recoveryPubKeyHex) {
109
- const pub = String(recoveryPubKeyHex || '').trim().toLowerCase().replace(/^0x/iu, '')
108
+ const pub = normalizeHex64(recoveryPubKeyHex)
110
109
  if (!isHex64(nodeHash) || !isHex64(pub)) return null
111
110
  return entityHashFromRecoveryPubKeyHex(nodeHash, pub)
112
111
  }
package/node/instance.mjs CHANGED
@@ -40,7 +40,7 @@ export function initNode(options) {
40
40
  if (runtime) throw new Error('p2p: initNode already called — use setNodeLogger / setSignalingRuntimeConfig or resetNodeForTests')
41
41
  if (options?.logger !== undefined || options?.signaling !== undefined)
42
42
  throw new Error('p2p: initNode only accepts nodeDir/entityStore — use setNodeLogger / setSignalingRuntimeConfig')
43
- const nodeDir = path.resolve(String(options.nodeDir || '').trim())
43
+ const nodeDir = path.resolve(options.nodeDir)
44
44
  if (!nodeDir) throw new Error('p2p: initNode requires nodeDir')
45
45
  const entityStore = options.entityStore ?? createFsEntityStore(path.join(nodeDir, 'entities'))
46
46
  runtime = {
package/node/log.mjs CHANGED
@@ -29,7 +29,7 @@ export function isConnectivityDebug() {
29
29
  * @returns {string} 短 hash
30
30
  */
31
31
  export function shortHash(hash, n = 8) {
32
- const value = String(hash || '')
32
+ const value = hash || ''
33
33
  return value.length <= n ? value : value.slice(0, n)
34
34
  }
35
35
 
package/node/network.mjs CHANGED
@@ -54,17 +54,17 @@ export function normalizeNetwork(raw) {
54
54
  */
55
55
  const pickIds = key => [...new Set(
56
56
  (file[key] || [])
57
- .map(id => normalizeHex64(id) || id.trim())
57
+ .map(id => normalizeHex64(id))
58
58
  .filter(id => isHex64(id)),
59
59
  )]
60
60
  const hints = (file.hints || [])
61
61
  .map(hint => ({
62
62
  nodeHash: normalizeHex64(hint.nodeHash) || '',
63
- source: hint.source?.trim() || '',
64
- kind: hint.kind?.trim() || '',
63
+ source: String(hint.source || ''),
64
+ kind: String(hint.kind || ''),
65
65
  weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
66
66
  expiresAt: Number(hint.expiresAt) || 0,
67
- ...hint.groupId ? { groupId: hint.groupId.trim() } : {},
67
+ ...hint.groupId ? { groupId: String(hint.groupId) } : {},
68
68
  }))
69
69
  .filter(hint => isHex64(hint.nodeHash))
70
70
  return {
@@ -98,7 +98,7 @@ export function capHintsBySource(hints, maxPerSource = MAX_HINTS_PER_SOURCE) {
98
98
  const counts = new Map()
99
99
  const out = []
100
100
  for (const hint of [...hints].reverse()) {
101
- const source = String(hint.source || 'unknown')
101
+ const source = hint.source || 'unknown'
102
102
  const n = counts.get(source) ?? 0
103
103
  if (n >= maxPerSource) continue
104
104
  counts.set(source, n + 1)
@@ -134,8 +134,8 @@ export function applyNetworkHint(hint) {
134
134
  const now = Date.now()
135
135
  const ttlMs = Number.isFinite(hint.ttlMs) ? hint.ttlMs : DEFAULT_EXPLORE_TTL_MS
136
136
  const expiresAt = Number.isFinite(hint.expiresAt) ? hint.expiresAt : now + ttlMs
137
- const source = String(hint.source || 'unknown')
138
- const priorSources = new Set(net.hints.filter(h => h.nodeHash === nodeHash).map(h => String(h.source || 'unknown')))
137
+ const source = hint.source || 'unknown'
138
+ const priorSources = new Set(net.hints.filter(h => h.nodeHash === nodeHash).map(h => h.source || 'unknown'))
139
139
  priorSources.add(source)
140
140
  const multiSourceBoost = priorSources.size >= 2 ? 1.2 : 1
141
141
  const baseWeight = Number.isFinite(hint.weight) ? hint.weight : 0.1
@@ -145,10 +145,10 @@ export function applyNetworkHint(hint) {
145
145
  net.hints.push({
146
146
  nodeHash,
147
147
  source,
148
- kind: String(hint.kind || 'hint'),
148
+ kind: hint.kind || 'hint',
149
149
  weight: baseWeight * multiSourceBoost,
150
150
  expiresAt,
151
- ...hint.groupId ? { groupId: String(hint.groupId).trim() } : {},
151
+ ...hint.groupId ? { groupId: hint.groupId } : {},
152
152
  })
153
153
  saveNetwork(net)
154
154
  }
@@ -234,11 +234,10 @@ export function replaceNetworkPeerPools(pools = {}) {
234
234
  * @returns {string[]} 规范化 value 列表
235
235
  */
236
236
  function denyValuesForScope(groupId, scope) {
237
- const gid = String(groupId || '').trim()
238
237
  return [...new Set(
239
238
  loadDenylist().blocked
240
239
  .filter(entry => entry.scope === scope)
241
- .filter(entry => scope === 'entity' || !entry.groupId || !gid || entry.groupId === gid)
240
+ .filter(entry => scope === 'entity' || !entry.groupId || !groupId || entry.groupId === groupId)
242
241
  .map(entry => entry.value),
243
242
  )]
244
243
  }
@@ -278,10 +277,9 @@ export function loadPeerPoolView(groupId = '') {
278
277
  * @returns {boolean} 是否命中 denylist(按 scope 匹配)
279
278
  */
280
279
  export function isPeerPoolKeyBlocked(view, key) {
281
- const normalized = String(key || '').trim().toLowerCase()
282
- if (!normalized) return false
283
- if (view.deniedNodes.includes(normalized)) return true
284
- if (view.deniedSubjects.includes(normalized)) return true
285
- if (isEntityHash128(normalized) && view.deniedEntities.includes(normalized)) return true
280
+ if (!key) return false
281
+ if (view.deniedNodes.includes(key)) return true
282
+ if (view.deniedSubjects.includes(key)) return true
283
+ if (isEntityHash128(key) && view.deniedEntities.includes(key)) return true
286
284
  return false
287
285
  }
@@ -38,12 +38,17 @@ export function normalizePersonalListEntries(raw) {
38
38
  /** @type {Map<string, { scope: PersonalListScope, value: string }>} */
39
39
  const byKey = new Map()
40
40
  for (const entry of raw || []) {
41
- const scope = String(entry?.scope || '').trim().toLowerCase()
42
- const value = String(entry?.value || '').trim().toLowerCase()
43
- if (scope === 'entity' && parseEntityHash(value))
44
- byKey.set(`entity:${value}`, { scope: 'entity', value })
45
- else if (scope === 'subject' && isHex64(normalizeHex64(value)))
46
- byKey.set(`subject:${normalizeHex64(value)}`, { scope: 'subject', value: normalizeHex64(value) })
41
+ const scope = String(entry?.scope || '')
42
+ if (scope === 'entity') {
43
+ const parsed = parseEntityHash(entry?.value)
44
+ if (parsed)
45
+ byKey.set(`entity:${parsed.entityHash}`, { scope: 'entity', value: parsed.entityHash })
46
+ }
47
+ else if (scope === 'subject') {
48
+ const value = normalizeHex64(entry?.value)
49
+ if (isHex64(value))
50
+ byKey.set(`subject:${value}`, { scope: 'subject', value })
51
+ }
47
52
  }
48
53
  return [...byKey.values()]
49
54
  }
@@ -58,7 +63,7 @@ export function normalizePersonalListEntries(raw) {
58
63
  * @returns {boolean} 是否命中列表
59
64
  */
60
65
  export function matchesPersonalListEntries(entries, subject) {
61
- const entity = String(subject?.entityHash || '').trim().toLowerCase()
66
+ const entity = subject?.entityHash || ''
62
67
  const pk = normalizeHex64(subject?.pubKeyHash || subject?.subjectHash || '')
63
68
  if (entity) {
64
69
  for (const entry of entries)
@@ -112,11 +117,10 @@ export async function loadPersonalBlockEntries(viewerEntityHash) {
112
117
  */
113
118
  export async function setPersonalHidden(viewerEntityHash, targetEntityHash, hide) {
114
119
  if (!isWritableLocalEntity(viewerEntityHash)) throw new Error('entity not writable on this replica')
115
- const target = String(targetEntityHash || '').trim().toLowerCase()
116
- if (!parseEntityHash(target)) throw new Error('invalid targetEntityHash')
120
+ if (!parseEntityHash(targetEntityHash)) throw new Error('invalid targetEntityHash')
117
121
  const store = getEntityStore()
118
122
  const current = normalizePersonalListEntries((await store.readEntityJson(viewerEntityHash, HIDE_JSON))?.hidden || [])
119
- const addEntries = entriesForTargetEntityHash(target)
123
+ const addEntries = entriesForTargetEntityHash(targetEntityHash)
120
124
  const addKeys = new Set(addEntries.map(e => `${e.scope}:${e.value}`))
121
125
  const next = hide
122
126
  ? normalizePersonalListEntries([...current, ...addEntries])
@@ -133,11 +137,10 @@ export async function setPersonalHidden(viewerEntityHash, targetEntityHash, hide
133
137
  */
134
138
  export async function setPersonalMuted(viewerEntityHash, targetEntityHash, mute) {
135
139
  if (!isWritableLocalEntity(viewerEntityHash)) throw new Error('entity not writable on this replica')
136
- const target = String(targetEntityHash || '').trim().toLowerCase()
137
- if (!parseEntityHash(target)) throw new Error('invalid targetEntityHash')
140
+ if (!parseEntityHash(targetEntityHash)) throw new Error('invalid targetEntityHash')
138
141
  const store = getEntityStore()
139
142
  const current = normalizePersonalListEntries((await store.readEntityJson(viewerEntityHash, MUTE_JSON))?.muted || [])
140
- const addEntries = entriesForTargetEntityHash(target)
143
+ const addEntries = entriesForTargetEntityHash(targetEntityHash)
141
144
  const addKeys = new Set(addEntries.map(e => `${e.scope}:${e.value}`))
142
145
  const next = mute
143
146
  ? normalizePersonalListEntries([...current, ...addEntries])
@@ -193,9 +196,9 @@ export function filterSetsFromPersonalListEntries(entries) {
193
196
  /** @type {Set<string>} */
194
197
  const mutedSubjects = new Set()
195
198
  for (const entry of entries || []) {
196
- const kind = String(entry?.kind || '').trim().toLowerCase()
197
- const scope = String(entry?.scope || '').trim().toLowerCase()
198
- const value = String(entry?.value || '').trim().toLowerCase()
199
+ const kind = String(entry?.kind || '')
200
+ const scope = String(entry?.scope || '')
201
+ const value = String(entry?.value || '')
199
202
  if (!value || (scope !== 'entity' && scope !== 'subject')) continue
200
203
  if (kind === 'block')
201
204
  if (scope === 'entity') blockedEntityHashes.add(value)
@@ -237,11 +240,10 @@ export async function loadPersonalFilterSets(viewerEntityHash) {
237
240
  * @returns {boolean} 是否应过滤
238
241
  */
239
242
  export function isAuthorFilteredByPersonalSets(filterSets, authorEntityHash) {
240
- const entity = String(authorEntityHash || '').trim().toLowerCase()
241
- if (!entity) return false
242
- if (filterSets.blockedEntityHashes.has(entity) || filterSets.hiddenEntityHashes.has(entity) || filterSets.mutedEntityHashes?.has(entity))
243
+ if (!authorEntityHash) return false
244
+ if (filterSets.blockedEntityHashes.has(authorEntityHash) || filterSets.hiddenEntityHashes.has(authorEntityHash) || filterSets.mutedEntityHashes?.has(authorEntityHash))
243
245
  return true
244
- const parsed = parseEntityHash(entity)
246
+ const parsed = parseEntityHash(authorEntityHash)
245
247
  if (!parsed) return false
246
248
  if (filterSets.blockedSubjects.has(parsed.subjectHash)
247
249
  || filterSets.hiddenSubjects.has(parsed.subjectHash)
@@ -120,7 +120,7 @@ export function recordGossipAllUnknownWant(groupId, peerNodeHash) {
120
120
  * @returns {Promise<void>}
121
121
  */
122
122
  export function recordMessageRateViolation(peerNodeHash, excessRatio = 1) {
123
- const id = String(peerNodeHash || '').trim()
123
+ const id = normalizeHex64(peerNodeHash)
124
124
  if (!id) return Promise.resolve()
125
125
  return mutateReputation(data => {
126
126
  recordMessageRateViolationPure(data, id, undefined, excessRatio)
@@ -134,7 +134,7 @@ export function recordMessageRateViolation(peerNodeHash, excessRatio = 1) {
134
134
  * @returns {Promise<boolean>} 是否触发异常隔离
135
135
  */
136
136
  export async function observePeerBehavior(peerNodeHash, sample) {
137
- const id = String(peerNodeHash || '').trim()
137
+ const id = normalizeHex64(peerNodeHash)
138
138
  if (!id) return false
139
139
  let anomaly = false
140
140
  await mutateReputation(data => {
@@ -149,7 +149,7 @@ export async function observePeerBehavior(peerNodeHash, sample) {
149
149
  * @returns {void}
150
150
  */
151
151
  export function bumpChunkStorageReputation(storagePeerKey) {
152
- const id = String(storagePeerKey || '').trim()
152
+ const id = normalizeHex64(storagePeerKey)
153
153
  if (!id) return
154
154
  void mutateReputation(data => {
155
155
  bumpChunkStorageReputationPure(data, id)
@@ -161,7 +161,7 @@ export function bumpChunkStorageReputation(storagePeerKey) {
161
161
  * @returns {void}
162
162
  */
163
163
  export function penalizeChunkStorageFailure(blamePeerKey) {
164
- const id = String(blamePeerKey || '').trim()
164
+ const id = normalizeHex64(blamePeerKey)
165
165
  if (!id) return
166
166
  void mutateReputation(data => {
167
167
  penalizeChunkStorageFailurePure(data, id)
@@ -173,24 +173,16 @@ export function penalizeChunkStorageFailure(blamePeerKey) {
173
173
  * @returns {void}
174
174
  */
175
175
  export function penalizeArchiveServeMismatch(peerNodeHash) {
176
- const id = String(peerNodeHash || '').trim()
176
+ const id = normalizeHex64(peerNodeHash)
177
177
  if (!id) return
178
178
  void mutateReputation(data => {
179
179
  penalizeArchiveServeMismatchPure(data, id)
180
180
  })
181
181
  }
182
182
 
183
- /**
184
- * @param {object | null | undefined} groupSettings 群设置
185
- * @returns {number} 毫秒
186
- */
187
- export function resolveSlashAlertTtlMs(groupSettings) {
188
- return resolveSlashAlertTtlMsPure(groupSettings)
189
- }
190
-
191
183
  /**
192
184
  * @param {object} alert VOLATILE slash 载荷
193
- * @returns {boolean} 是否已应用
185
+ * @returns {Promise<boolean>} 是否已应用
194
186
  */
195
187
  export async function applyVolatileSlashAlert(alert) {
196
188
  const expiresAt = Number(alert?.expiresAt)
@@ -215,7 +207,7 @@ export function buildUnverifiedSlashAlert(senderPubKeyHash, content, groupSettin
215
207
  const targetPubKeyHash = assertHex64(content.targetPubKeyHash, 'slash target')
216
208
  const claim = Number.isFinite(Number(content.claim)) ? Number(content.claim) : reputationTunables.slashDefaultClaim
217
209
  const sender = assertHex64(senderPubKeyHash, 'slash sender')
218
- const ttl = resolveSlashAlertTtlMs(groupSettings)
210
+ const ttl = resolveSlashAlertTtlMsPure(groupSettings)
219
211
  return {
220
212
  type: 'reputation_slash_alert',
221
213
  targetPubKeyHash,
@@ -235,8 +227,8 @@ export function buildUnverifiedSlashAlert(senderPubKeyHash, content, groupSettin
235
227
  export async function applySubjectiveSlashFromEvent(username, groupId, event, readEvents) {
236
228
  if (event.type !== 'reputation_slash') return
237
229
  const { content } = event
238
- const target = content.targetPubKeyHash.trim().toLowerCase()
239
- const sender = event.sender.trim().toLowerCase()
230
+ const target = normalizeHex64(content.targetPubKeyHash)
231
+ const sender = normalizeHex64(event.sender)
240
232
 
241
233
  await mutateReputation(async data => {
242
234
  const verified = content.verified && await verifySlashProof(username, groupId, content, readEvents)
@@ -253,7 +245,7 @@ export async function applySubjectiveSlashFromEvent(username, groupId, event, re
253
245
  * @returns {Promise<boolean>} 是否找到 proof 对应事件
254
246
  */
255
247
  async function verifySlashProof(username, groupId, content, readEvents) {
256
- const eventId = content?.proof?.eventId?.trim().toLowerCase()
248
+ const eventId = normalizeHex64(content?.proof?.eventId)
257
249
  if (!eventId) return false
258
250
  const events = await readEvents(username, groupId)
259
251
  return events.some(event => event.id === eventId)
@@ -269,7 +261,7 @@ export function applyDecayCollusionAfterSlash(targetPubKeyHash, inviteEdges) {
269
261
  const applied = applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges)
270
262
  if (applied.length)
271
263
  getNodeLogger()?.warn?.('reputation: collusion decay after slash', {
272
- target: targetPubKeyHash.trim().toLowerCase(),
264
+ target: normalizeHex64(targetPubKeyHash),
273
265
  upstreamCount: applied.length,
274
266
  hops: applied.map(row => row.hop),
275
267
  })
@@ -85,13 +85,6 @@ function persistSyncConfig() {
85
85
  writeNodeJsonSync(SYNC_DATA_NAME, loadSyncConfig())
86
86
  }
87
87
 
88
- /**
89
- * @returns {object} 当前 reputation 表(byNodeHash)
90
- */
91
- export function getReputationTable() {
92
- return loadReputation()
93
- }
94
-
95
88
  /**
96
89
  * @param {object} table - 含 byNodeHash 的信誉表或裸 byNodeHash 对象
97
90
  * @returns {void}
@@ -41,7 +41,7 @@ export function resolveSignalingRuntimeConfig(patch = {}) {
41
41
  relayOverride = patch.relayOverride == null
42
42
  ? null
43
43
  : [...new Set((Array.isArray(patch.relayOverride) ? patch.relayOverride : [])
44
- .map(url => String(url || '').trim())
44
+ .map(url => String(url || ''))
45
45
  .filter(url => url.startsWith('wss://')))]
46
46
  return {
47
47
  relayOverride,
@@ -47,7 +47,7 @@ export function createLocalStoragePlugin(baseDir) {
47
47
  * @returns {Promise<Uint8Array>} 文件内容
48
48
  */
49
49
  async getChunk(locator) {
50
- const localLocatorMatch = String(locator).match(/^local:([^/]+)\/chunks\/(.+)$/)
50
+ const localLocatorMatch = locator.match(/^local:([^/]+)\/chunks\/(.+)$/)
51
51
  if (!localLocatorMatch) throw new Error('Invalid local locator')
52
52
  const chunkPath = join(baseDir, 'groups', localLocatorMatch[1], 'chunks', localLocatorMatch[2])
53
53
  return new Uint8Array(await readFile(chunkPath))
@@ -57,7 +57,7 @@ export function createLocalStoragePlugin(baseDir) {
57
57
  * @returns {Promise<void>}
58
58
  */
59
59
  async deleteChunk(locator) {
60
- const localLocatorMatch = String(locator).match(/^local:([^/]+)\/chunks\/(.+)$/)
60
+ const localLocatorMatch = locator.match(/^local:([^/]+)\/chunks\/(.+)$/)
61
61
  if (!localLocatorMatch) return
62
62
  const chunkPath = join(baseDir, 'groups', localLocatorMatch[1], 'chunks', localLocatorMatch[2])
63
63
  try {