@steve02081504/fount-p2p 0.0.26 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/core/bytes_codec.mjs +5 -6
- package/core/composite_key.mjs +0 -79
- package/core/entity_id_parse.mjs +2 -3
- package/core/evfs_logical_path.mjs +2 -3
- package/core/hexIds.mjs +5 -5
- package/core/logical_entity.mjs +3 -4
- package/crypto/channel.mjs +2 -25
- package/crypto/checkpoint_sign.mjs +5 -4
- package/crypto/crypto.mjs +0 -8
- package/crypto/key.mjs +9 -10
- package/dag/canonicalize_row.mjs +1 -1
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +7 -12
- package/discovery/bt/index.mjs +6 -11
- package/discovery/bt/peer_hints.mjs +2 -3
- package/discovery/bt/runtime.mjs +1 -1
- package/discovery/index.mjs +14 -14
- package/discovery/lan.mjs +3 -3
- package/discovery/lan_interfaces.mjs +2 -2
- package/discovery/lan_peer_hints.mjs +1 -1
- package/discovery/nostr.mjs +19 -24
- package/federation/entity_key_chain.mjs +5 -24
- package/federation/message_rate_limit.mjs +3 -2
- package/federation/part_query/runtime.mjs +9 -20
- package/files/assemble.mjs +3 -3
- package/files/assemble_stream.mjs +1 -1
- package/files/chunk/fetch.mjs +1 -2
- package/files/chunk/responder.mjs +4 -41
- package/files/chunk/store.mjs +2 -3
- package/files/chunk/verify.mjs +2 -3
- package/files/evfs.mjs +1 -1
- package/files/evfs_ref.mjs +3 -4
- package/files/fetch_fanout.mjs +3 -3
- package/files/manifest/fetch.mjs +2 -3
- package/files/manifest/normalize.mjs +9 -9
- package/files/manifest/pending.mjs +1 -4
- package/files/manifest/public.mjs +8 -8
- package/files/transfer_key.mjs +3 -3
- package/files/transfer_key_registry.mjs +6 -6
- package/governance/branch.mjs +2 -1
- package/governance/join_pow.mjs +7 -5
- package/governance/owner_succession_ballot.mjs +6 -6
- package/index.mjs +8 -2
- package/infra/default_node_dir.mjs +2 -2
- package/infra/priority.mjs +2 -2
- package/link/frame.mjs +2 -3
- package/link/handshake.mjs +6 -6
- package/link/pipe.mjs +69 -5
- package/link/providers/ble_gatt.mjs +1 -9
- package/link/providers/index.mjs +2 -2
- package/link/providers/nostr.mjs +1 -3
- package/link/providers/webrtc.mjs +2 -2
- package/link/rtc/ice_local_hostname.mjs +6 -5
- package/link/rtc/w3c_bridge.mjs +1 -1
- package/link/sdp_fingerprint.mjs +1 -1
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +4 -4
- package/mailbox/parse.mjs +1 -1
- package/mailbox/prune.mjs +2 -2
- package/mailbox/rate.mjs +3 -10
- package/mailbox/store.mjs +7 -7
- package/node/denylist.mjs +38 -38
- package/node/entity_store.mjs +5 -5
- package/node/identity.mjs +7 -8
- package/node/instance.mjs +1 -1
- package/node/log.mjs +1 -1
- package/node/network.mjs +14 -16
- package/node/personal_block.mjs +22 -20
- package/node/reputation_store.mjs +11 -19
- package/node/reputation_sync.mjs +0 -7
- package/node/signaling_config.mjs +1 -1
- package/node/storage_plugins.mjs +2 -2
- package/overlay/index.mjs +1 -1
- package/package.json +1 -1
- package/registries/event_type.mjs +2 -2
- package/registries/room_provider.mjs +2 -2
- package/reputation/engine.mjs +17 -15
- package/schemas/discovery.mjs +5 -5
- package/schemas/federation_pull.mjs +16 -24
- package/schemas/part_query.mjs +4 -4
- package/timeline/verify_remote.mjs +4 -4
- package/transport/group_link_set.mjs +3 -3
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +51 -20
- package/transport/mesh_keepalive.mjs +1 -1
- package/transport/node_scope/wire.mjs +1 -1
- package/transport/peer_health.mjs +127 -0
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +6 -6
- package/transport/runtime_bootstrap.mjs +12 -12
- package/trust_graph/cache.mjs +2 -3
- package/trust_graph/engine.mjs +1 -1
- package/trust_graph/registry.mjs +2 -2
- package/trust_graph/send.mjs +1 -1
- package/utils/async_mutex.mjs +1 -1
- package/wire/ingress.mjs +1 -1
- package/wire/part/ingress.mjs +6 -6
- package/wire/part/query.mjs +3 -3
- package/wire/volatile_signature.mjs +4 -9
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 || '')
|
|
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 || '')
|
|
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 || '')
|
|
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
|
|
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:
|
|
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)
|
|
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)
|
|
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 =
|
|
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 || '')
|
|
79
|
-
const
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 || '')
|
|
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 || '')
|
|
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 (!
|
|
144
|
-
if (isHex64(pk) && keys.has(denyKey('subject',
|
|
145
|
-
if (isHex64(node) && keys.has(denyKey('node',
|
|
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 (!
|
|
172
|
-
if (keys.has(denyKey('subject',
|
|
173
|
-
if (keys.has(denyKey('node',
|
|
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 =
|
|
200
|
-
|
|
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
|
-
|
|
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(
|
|
210
|
+
if (scope === 'entity' && !isEntityHash128(normValue))
|
|
208
211
|
throw new Error('invalid entityHash')
|
|
209
|
-
if (scope === 'node' && !isHex64(
|
|
212
|
+
if (scope === 'node' && !isHex64(normValue))
|
|
210
213
|
throw new Error('invalid nodeHash')
|
|
211
214
|
|
|
212
|
-
const
|
|
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')
|
|
230
|
-
const sourceGroupId =
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
262
|
-
|
|
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 !==
|
|
268
|
-
if (
|
|
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)
|
package/node/entity_store.mjs
CHANGED
|
@@ -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
|
|
29
|
-
if (!
|
|
30
|
-
return
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
51
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
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)
|
|
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
|
|
64
|
-
kind: hint.kind
|
|
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
|
|
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 =
|
|
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 =
|
|
138
|
-
const priorSources = new Set(net.hints.filter(h => h.nodeHash === nodeHash).map(h =>
|
|
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:
|
|
148
|
+
kind: hint.kind || 'hint',
|
|
149
149
|
weight: baseWeight * multiSourceBoost,
|
|
150
150
|
expiresAt,
|
|
151
|
-
...hint.groupId ? { groupId:
|
|
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 || !
|
|
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
|
-
|
|
282
|
-
if (
|
|
283
|
-
if (view.
|
|
284
|
-
if (view.
|
|
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
|
}
|
package/node/personal_block.mjs
CHANGED
|
@@ -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 || '')
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 || '')
|
|
197
|
-
const scope = String(entry?.scope || '')
|
|
198
|
-
const value = String(entry?.value || '')
|
|
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
|
-
|
|
241
|
-
if (
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
239
|
-
const sender = event.sender
|
|
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
|
|
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
|
|
264
|
+
target: normalizeHex64(targetPubKeyHash),
|
|
273
265
|
upstreamCount: applied.length,
|
|
274
266
|
hops: applied.map(row => row.hop),
|
|
275
267
|
})
|
package/node/reputation_sync.mjs
CHANGED
|
@@ -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 || '')
|
|
44
|
+
.map(url => String(url || ''))
|
|
45
45
|
.filter(url => url.startsWith('wss://')))]
|
|
46
46
|
return {
|
|
47
47
|
relayOverride,
|
package/node/storage_plugins.mjs
CHANGED
|
@@ -47,7 +47,7 @@ export function createLocalStoragePlugin(baseDir) {
|
|
|
47
47
|
* @returns {Promise<Uint8Array>} 文件内容
|
|
48
48
|
*/
|
|
49
49
|
async getChunk(locator) {
|
|
50
|
-
const localLocatorMatch =
|
|
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 =
|
|
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 {
|