@steve02081504/fount-p2p 0.0.35 → 0.0.37
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/AGENTS.md +2 -2
- package/core/entity_id.mjs +6 -6
- package/core/entity_id_parse.mjs +7 -7
- package/core/hexIds.mjs +6 -18
- package/crypto/checkpoint_sign.mjs +4 -4
- package/dag/canonicalize_row.mjs +4 -7
- package/discovery/adverts.mjs +17 -10
- package/discovery/bt/index.mjs +11 -11
- package/discovery/bt/peer_hints.mjs +4 -7
- package/discovery/index.mjs +8 -12
- package/discovery/internal/signal_crypto.mjs +1 -2
- package/discovery/lan.mjs +7 -9
- package/discovery/lan_peer_hints.mjs +5 -8
- package/discovery/nostr/census.mjs +281 -0
- package/discovery/nostr/census_math.mjs +54 -0
- package/discovery/nostr/constants.mjs +43 -0
- package/discovery/nostr/index.mjs +612 -0
- package/discovery/nostr/relays.mjs +1071 -0
- package/discovery/nostr/selection.mjs +224 -0
- package/discovery/nostr/session.mjs +584 -0
- package/discovery/peer_clue.mjs +1 -4
- package/docs/nostr_relay_discovery.md +92 -0
- package/federation/entity_key_chain.mjs +10 -9
- package/federation/message_rate_limit.mjs +1 -3
- package/files/manifest/fetch.mjs +4 -6
- package/files/manifest/normalize.mjs +2 -2
- package/files/manifest/public.mjs +5 -5
- package/governance/branch.mjs +1 -2
- package/governance/join_pow.mjs +2 -4
- package/governance/owner_succession_ballot.mjs +5 -5
- package/index.mjs +11 -4
- package/infra/priority.mjs +1 -2
- package/link/handshake.mjs +153 -41
- package/link/pipe.mjs +1 -2
- package/link/providers/ble_gatt.mjs +2 -3
- package/link/providers/lan_tcp.mjs +2 -3
- package/link/providers/link_id_pipe.mjs +3 -3
- package/link/providers/{nostr.mjs → nostr/index.mjs} +22 -17
- package/mailbox/deliver_or_store.mjs +11 -12
- package/mailbox/prune.mjs +3 -4
- package/mailbox/rate.mjs +3 -2
- package/mailbox/store.mjs +6 -9
- package/node/denylist.mjs +25 -28
- package/node/feature_config.mjs +30 -0
- package/node/identity.mjs +6 -6
- package/node/instance.mjs +21 -0
- package/node/network.mjs +12 -12
- package/node/personal_block.mjs +4 -4
- package/node/reputation_store.mjs +13 -14
- package/node/reputation_sync.mjs +9 -10
- package/node/storage.mjs +23 -0
- package/package.json +10 -4
- package/pages/app.mjs +172 -0
- package/pages/index.html +36 -0
- package/reputation/engine.mjs +13 -16
- package/schemas/discovery.mjs +5 -6
- package/schemas/federation_pull.mjs +7 -7
- package/schemas/mailbox.mjs +3 -3
- package/schemas/part_query.mjs +5 -5
- package/timeline/verify_remote.mjs +7 -7
- package/transport/link_registry.mjs +16 -19
- package/transport/mesh_keepalive.mjs +8 -11
- package/transport/offer_answer.mjs +3 -3
- package/transport/peer_health.mjs +12 -15
- package/transport/runtime_bootstrap.mjs +39 -3
- package/trust_graph/send.mjs +3 -3
- package/discovery/nostr.mjs +0 -1074
|
@@ -2,7 +2,6 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
import net from 'node:net'
|
|
4
4
|
|
|
5
|
-
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
5
|
import { getLanPeerHint, listLanPeerHints } from '../../discovery/lan_peer_hints.mjs'
|
|
7
6
|
import { asLinkHandle } from '../pipe.mjs'
|
|
8
7
|
|
|
@@ -186,7 +185,7 @@ async function openTcpPipe(options) {
|
|
|
186
185
|
pipe.handleInbound(payload)
|
|
187
186
|
|
|
188
187
|
if (options.initiator)
|
|
189
|
-
codec.write(buildLinkOpen(
|
|
188
|
+
codec.write(buildLinkOpen(options.linkId, options.localIdentity?.nodeHash))
|
|
190
189
|
|
|
191
190
|
await pipe.startHandshake()
|
|
192
191
|
return asLinkHandle(pipe)
|
|
@@ -198,7 +197,7 @@ async function openTcpPipe(options) {
|
|
|
198
197
|
* @returns {Promise<import('./index.mjs').LinkHandle>} 已就绪的 link
|
|
199
198
|
*/
|
|
200
199
|
async function dialLanTcp(options) {
|
|
201
|
-
const remoteNodeHash =
|
|
200
|
+
const remoteNodeHash = options.nodeHash
|
|
202
201
|
const hints = listLanPeerHints(remoteNodeHash)
|
|
203
202
|
if (!hints.length) throw new Error('p2p: lan_tcp no peer hint')
|
|
204
203
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isHex64 } from '../../core/hexIds.mjs'
|
|
4
4
|
import { createLinkPipe } from '../pipe.mjs'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,7 +25,7 @@ export function parseLinkOpen(raw) {
|
|
|
25
25
|
}
|
|
26
26
|
catch { return null }
|
|
27
27
|
if (parsed?.type !== 'link-open' || !parsed.linkId) return null
|
|
28
|
-
return { linkId: String(parsed.linkId), from:
|
|
28
|
+
return { linkId: String(parsed.linkId), from: isHex64(parsed.from) }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -34,7 +34,7 @@ export function parseLinkOpen(raw) {
|
|
|
34
34
|
* @returns {ReturnType<typeof createLinkPipe>} link pipe
|
|
35
35
|
*/
|
|
36
36
|
export function createLinkIdBoundPipe(options) {
|
|
37
|
-
const linkId =
|
|
37
|
+
const { linkId } = options
|
|
38
38
|
if (!linkId) throw new Error(`p2p: ${options.providerId || 'link'} linkId required`)
|
|
39
39
|
return createLinkPipe({
|
|
40
40
|
...options,
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
|
-
import { base64ToBytes, bytesToBase64 } from '
|
|
5
|
-
import {
|
|
6
|
-
import { getDiscoveryProvider, sendNodeSignalPacket } from '
|
|
7
|
-
import { NOSTR_SIGNAL_KIND, resolveNostrRelayUrls } from '
|
|
8
|
-
import { ms } from '
|
|
9
|
-
import { createLruMap } from '
|
|
10
|
-
import { FRAME_HEADER_BYTES, maxFrameChunkBytesForPayload } from '
|
|
11
|
-
import { asLinkHandle } from '
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import { createLinkIdBoundPipe } from './link_id_pipe.mjs'
|
|
4
|
+
import { base64ToBytes, bytesToBase64 } from '../../../core/bytes_codec.mjs'
|
|
5
|
+
import { isHex64 } from '../../../core/hexIds.mjs'
|
|
6
|
+
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../../discovery/index.mjs'
|
|
7
|
+
import { NOSTR_SIGNAL_KIND, resolveNostrRelayUrls } from '../../../discovery/nostr/index.mjs'
|
|
8
|
+
import { ms } from '../../../utils/duration.mjs'
|
|
9
|
+
import { createLruMap } from '../../../utils/lru.mjs'
|
|
10
|
+
import { FRAME_HEADER_BYTES, maxFrameChunkBytesForPayload } from '../../frame.mjs'
|
|
11
|
+
import { asLinkHandle } from '../../pipe.mjs'
|
|
12
|
+
import { LINK_LEVEL_NOSTR } from '../levels.mjs'
|
|
13
|
+
import { createLinkIdBoundPipe } from '../link_id_pipe.mjs'
|
|
15
14
|
|
|
16
15
|
/** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。
|
|
17
16
|
* 默认兜底取 2026-08 本机对默认公共 relay 的 NIP-11 `max_message_length` 非零最小值(131072 = nostr.mom)。
|
|
@@ -317,9 +316,8 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
317
316
|
*/
|
|
318
317
|
async function deliverPacket(packet) {
|
|
319
318
|
if (packet?.type !== 'link') return
|
|
320
|
-
const linkId =
|
|
321
|
-
|
|
322
|
-
if (!linkId || !from) return
|
|
319
|
+
const { linkId, from } = packet
|
|
320
|
+
if (!isHex64(linkId) || !isHex64(from)) return
|
|
323
321
|
if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
|
|
324
322
|
|
|
325
323
|
const op = String(packet.op || '')
|
|
@@ -375,8 +373,8 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
375
373
|
*/
|
|
376
374
|
async dial(dialOptions) {
|
|
377
375
|
if (!isAvailable()) return null
|
|
378
|
-
const remoteNodeHash =
|
|
379
|
-
if (!remoteNodeHash) return null
|
|
376
|
+
const remoteNodeHash = dialOptions.nodeHash
|
|
377
|
+
if (!isHex64(remoteNodeHash)) return null
|
|
380
378
|
localIdentity = dialOptions.localIdentity || localIdentity
|
|
381
379
|
if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
|
|
382
380
|
await refreshPayloadCap(resolveRelayUrls())
|
|
@@ -398,8 +396,15 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
398
396
|
ensureListening(handlers) {
|
|
399
397
|
onInbound = handlers.onInbound
|
|
400
398
|
localIdentity = handlers.localIdentity
|
|
401
|
-
|
|
399
|
+
// 探测延迟到启动路径之外(首个 macrotask),避免同步 fetch 阻塞 startup 预算。
|
|
400
|
+
const capTimer = setTimeout(() => {
|
|
401
|
+
let relayUrls
|
|
402
|
+
try { relayUrls = resolveRelayUrls() } catch { return }
|
|
403
|
+
void refreshPayloadCap(relayUrls).catch(() => { })
|
|
404
|
+
}, 0)
|
|
405
|
+
capTimer.unref?.()
|
|
402
406
|
return () => {
|
|
407
|
+
clearTimeout(capTimer)
|
|
403
408
|
onInbound = null
|
|
404
409
|
for (const session of sessions.values())
|
|
405
410
|
void session.pipe.close('listen-stop')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
3
|
import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
|
|
4
4
|
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
26
|
if (!peerId) return null
|
|
27
27
|
const entry = activeLinkRoster().find(row => row.peerId === peerId)
|
|
28
|
-
const remote =
|
|
28
|
+
const remote = entry?.remoteNodeHash
|
|
29
29
|
return remote || null
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -62,8 +62,8 @@ async function resolveRouting(username) {
|
|
|
62
62
|
*/
|
|
63
63
|
export async function deliverOrStoreMailboxPut(username, options) {
|
|
64
64
|
const routing = await resolveRouting(username)
|
|
65
|
-
const toPubKeyHash =
|
|
66
|
-
if (!toPubKeyHash) return { stored: false, delivered: false, relayed: 0 }
|
|
65
|
+
const { toPubKeyHash } = options
|
|
66
|
+
if (!isHex64(toPubKeyHash)) return { stored: false, delivered: false, relayed: 0 }
|
|
67
67
|
const hop = normalizeMailboxHop(options.hop)
|
|
68
68
|
if (hop >= routing.maxHop) return { stored: false, delivered: false, relayed: 0 }
|
|
69
69
|
const tier = mailboxTierFromHop(hop)
|
|
@@ -76,9 +76,8 @@ export async function deliverOrStoreMailboxPut(username, options) {
|
|
|
76
76
|
fromNodeHash: options.record?.fromNodeHash || nodeHash,
|
|
77
77
|
}
|
|
78
78
|
const stored = await storeMailboxRecord(record)
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
79
|
+
const delivered = isHex64(options.toNodeHash) && isMailboxRecordWithinSizeLimit(record)
|
|
80
|
+
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, options.toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
82
81
|
: false
|
|
83
82
|
|
|
84
83
|
let relayed = 0
|
|
@@ -114,8 +113,8 @@ export async function publishMailboxRecord(username, toPubKeyHash, record, toNod
|
|
|
114
113
|
export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
115
114
|
const { record } = put
|
|
116
115
|
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
117
|
-
const fromNode =
|
|
118
|
-
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
116
|
+
const fromNode = put.nodeHash
|
|
117
|
+
if (!isHex64(fromNode) || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
119
118
|
const username = wireContext.replicaUsername || ''
|
|
120
119
|
if (!username) return
|
|
121
120
|
if (peerId) {
|
|
@@ -143,8 +142,8 @@ export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
|
143
142
|
*/
|
|
144
143
|
export async function respondMailboxWant(want, sendGive, peerId) {
|
|
145
144
|
const { getMailboxRecords, takeMailboxForRecipient } = await import('./store.mjs')
|
|
146
|
-
const recipient =
|
|
147
|
-
if (!recipient) return
|
|
145
|
+
const recipient = want.toPubKeyHash
|
|
146
|
+
if (!isHex64(recipient)) return
|
|
148
147
|
const ids = want.ids || []
|
|
149
148
|
const rows = (ids.length
|
|
150
149
|
? await getMailboxRecords(ids)
|
|
@@ -179,7 +178,7 @@ export async function ingestMailboxGive(wireContext, give) {
|
|
|
179
178
|
export async function requestMailboxFromNetwork(username, toPubKeyHash) {
|
|
180
179
|
const routing = await resolveRouting(username)
|
|
181
180
|
const { listMailboxIdsForRecipient } = await import('./store.mjs')
|
|
182
|
-
const recipient =
|
|
181
|
+
const recipient = toPubKeyHash
|
|
183
182
|
if (!recipient) return
|
|
184
183
|
await deliverToUserRoomPeers(username, 'mailbox_want', {
|
|
185
184
|
toPubKeyHash: recipient,
|
package/mailbox/prune.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mailbox 公平淘汰(纯函数,供 store 与单测复用)。
|
|
3
3
|
*/
|
|
4
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
4
|
|
|
6
5
|
/** 单收件人×发送者桶:条数上限 */
|
|
7
6
|
export const MAX_BUCKET_ENTRIES = 10
|
|
@@ -31,9 +30,9 @@ export function mailboxRecordBytes(record) {
|
|
|
31
30
|
* @returns {string} 公平淘汰桶键
|
|
32
31
|
*/
|
|
33
32
|
export function mailboxBucketKey(record) {
|
|
34
|
-
const to =
|
|
35
|
-
const from =
|
|
36
|
-
||
|
|
33
|
+
const to = record.toPubKeyHash || ''
|
|
34
|
+
const from = record.fromNodeHash
|
|
35
|
+
|| record.envelope?.sender
|
|
37
36
|
|| 'unknown'
|
|
38
37
|
return `${to}\0${from}`
|
|
39
38
|
}
|
package/mailbox/rate.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mailbox 入站 put 限速(按来源节点,节点级单例)。
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
5
|
|
|
6
6
|
const DEFAULT_WINDOW_MS = 60_000
|
|
7
7
|
const DEFAULT_MAX_PUTS = 20
|
|
@@ -62,7 +62,8 @@ function evictLruIfNeeded(key) {
|
|
|
62
62
|
*/
|
|
63
63
|
export function takeIncomingMailboxPutSlot(fromNodeHash, limits) {
|
|
64
64
|
const { windowMs, maxPuts } = resolveMailboxRateLimits(limits)
|
|
65
|
-
const key =
|
|
65
|
+
const key = fromNodeHash
|
|
66
|
+
if (!isHex64(key)) return false
|
|
66
67
|
const now = Date.now()
|
|
67
68
|
if (inboundByKey.size >= MAX_KEYS) sweepExpiredEntries(now)
|
|
68
69
|
evictLruIfNeeded(key)
|
package/mailbox/store.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile } from 'node:fs/promises'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { isHex64
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
5
|
import { jsonlMutexKey, writeJsonl } from '../dag/storage.mjs'
|
|
6
6
|
import { mailboxStorePath } from '../node/user_paths.mjs'
|
|
7
7
|
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
@@ -150,7 +150,7 @@ export function mailboxEnvelopeId(envelope) {
|
|
|
150
150
|
*/
|
|
151
151
|
export async function storeMailboxRecord(record) {
|
|
152
152
|
if (!isMailboxRecordWithinSizeLimit(record)) return false
|
|
153
|
-
const toPubKeyHash =
|
|
153
|
+
const { toPubKeyHash } = record
|
|
154
154
|
if (!isHex64(toPubKeyHash)) return false
|
|
155
155
|
const hop = normalizeMailboxHop(record.hop)
|
|
156
156
|
const { tier } = record
|
|
@@ -178,7 +178,7 @@ export async function storeMailboxRecord(record) {
|
|
|
178
178
|
envelope: record.envelope,
|
|
179
179
|
storedAt: Date.now(),
|
|
180
180
|
expiresAt: Date.now() + ttlMs,
|
|
181
|
-
fromNodeHash:
|
|
181
|
+
fromNodeHash: record.fromNodeHash,
|
|
182
182
|
hop,
|
|
183
183
|
tier,
|
|
184
184
|
importance: Number.isFinite(Number(record.importance)) ? Number(record.importance) : undefined,
|
|
@@ -193,9 +193,8 @@ export async function storeMailboxRecord(record) {
|
|
|
193
193
|
* @returns {Promise<string[]>} record id 列表
|
|
194
194
|
*/
|
|
195
195
|
export async function listMailboxIdsForRecipient(toPubKeyHash) {
|
|
196
|
-
const recipient = normalizeHex64(toPubKeyHash)
|
|
197
196
|
return (await readAll())
|
|
198
|
-
.filter(record => record.toPubKeyHash ===
|
|
197
|
+
.filter(record => record.toPubKeyHash === toPubKeyHash)
|
|
199
198
|
.map(record => record.id)
|
|
200
199
|
}
|
|
201
200
|
|
|
@@ -224,8 +223,7 @@ export async function deleteMailboxRecords(ids) {
|
|
|
224
223
|
* @returns {Promise<MailboxRecord[]>} 待发记录(不删除)
|
|
225
224
|
*/
|
|
226
225
|
export async function takeMailboxForRecipient(toPubKeyHash) {
|
|
227
|
-
|
|
228
|
-
return (await readAll()).filter(record => record.toPubKeyHash === recipient)
|
|
226
|
+
return (await readAll()).filter(record => record.toPubKeyHash === toPubKeyHash)
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
/**
|
|
@@ -233,9 +231,8 @@ export async function takeMailboxForRecipient(toPubKeyHash) {
|
|
|
233
231
|
* @returns {Promise<number>} 未过期条数
|
|
234
232
|
*/
|
|
235
233
|
export async function countMailboxPendingForRecipient(toPubKeyHash) {
|
|
236
|
-
const recipient = normalizeHex64(toPubKeyHash)
|
|
237
234
|
const now = Date.now()
|
|
238
|
-
return (await readAll()).filter(record => record.toPubKeyHash ===
|
|
235
|
+
return (await readAll()).filter(record => record.toPubKeyHash === toPubKeyHash && record.expiresAt > now).length
|
|
239
236
|
}
|
|
240
237
|
|
|
241
238
|
/**
|
package/node/denylist.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compositeKey } from '../core/composite_key.mjs'
|
|
2
2
|
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
3
|
-
import { isHex64
|
|
3
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
5
5
|
|
|
6
6
|
import { readNodeJsonSync, writeNodeJsonSync } from './storage.mjs'
|
|
@@ -79,15 +79,15 @@ export function normalizeDenylist(raw) {
|
|
|
79
79
|
const groupId = String(entry.groupId || '')
|
|
80
80
|
if (!scope) continue
|
|
81
81
|
if (scope === 'entity') {
|
|
82
|
-
const value =
|
|
83
|
-
if (
|
|
82
|
+
const value = isEntityHash128(entry?.value)
|
|
83
|
+
if (value)
|
|
84
84
|
blocked.push({ scope: 'entity', value })
|
|
85
85
|
continue
|
|
86
86
|
}
|
|
87
|
-
const value =
|
|
88
|
-
if (scope === 'node' &&
|
|
87
|
+
const value = isHex64(entry?.value)
|
|
88
|
+
if (scope === 'node' && value)
|
|
89
89
|
blocked.push({ scope: 'node', value, ...groupId ? { groupId } : {} })
|
|
90
|
-
else if (scope === 'subject' &&
|
|
90
|
+
else if (scope === 'subject' && value)
|
|
91
91
|
blocked.push({ scope: 'subject', value, ...groupId ? { groupId } : {} })
|
|
92
92
|
}
|
|
93
93
|
return { blocked }
|
|
@@ -116,12 +116,12 @@ export function saveDenylist(list) {
|
|
|
116
116
|
* @returns {boolean} 是否命中群级 ban 集合
|
|
117
117
|
*/
|
|
118
118
|
export function isSubjectBannedByState(state, subject) {
|
|
119
|
-
const pk =
|
|
120
|
-
if (
|
|
121
|
-
const entity =
|
|
122
|
-
if (
|
|
123
|
-
const node =
|
|
124
|
-
if (
|
|
119
|
+
const pk = isHex64(subject?.pubKeyHash)
|
|
120
|
+
if (pk && state?.bannedMembers?.has?.(pk)) return true
|
|
121
|
+
const entity = isEntityHash128(subject?.entityHash)
|
|
122
|
+
if (entity && state?.bannedEntities?.has?.(entity)) return true
|
|
123
|
+
const node = isHex64(subject?.nodeHash)
|
|
124
|
+
if (node && state?.bannedNodes?.has?.(node)) return true
|
|
125
125
|
return false
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -132,17 +132,17 @@ export function isSubjectBannedByState(state, subject) {
|
|
|
132
132
|
* @returns {boolean} 是否命中
|
|
133
133
|
*/
|
|
134
134
|
function matchesDenylistIndex(index, subject, groupId = '') {
|
|
135
|
-
const pk =
|
|
135
|
+
const pk = isHex64(subject?.pubKeyHash)
|
|
136
136
|
const entity = String(subject?.entityHash || '')
|
|
137
|
-
const node =
|
|
137
|
+
const node = isHex64(subject?.nodeHash)
|
|
138
138
|
const { keys } = index
|
|
139
139
|
|
|
140
140
|
if (entity && keys.has(denyKey('entity', '*', entity))) return true
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
141
|
+
if (node && keys.has(denyKey('node', '*', node))) return true
|
|
142
|
+
if (pk && keys.has(denyKey('subject', '*', pk))) return true
|
|
143
143
|
if (!groupId) return false
|
|
144
|
-
if (
|
|
145
|
-
if (
|
|
144
|
+
if (pk && keys.has(denyKey('subject', groupId, pk))) return true
|
|
145
|
+
if (node && keys.has(denyKey('node', groupId, node))) return true
|
|
146
146
|
return false
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -161,8 +161,8 @@ export function isSubjectBlocked(subject, groupId = '') {
|
|
|
161
161
|
* @returns {boolean} 是否拉黑
|
|
162
162
|
*/
|
|
163
163
|
export function isPeerKeyBlocked(groupId, peerKey) {
|
|
164
|
-
const key =
|
|
165
|
-
if (!
|
|
164
|
+
const key = isHex64(peerKey)
|
|
165
|
+
if (!key) return false
|
|
166
166
|
const index = getDenylistIndex()
|
|
167
167
|
const { keys } = index
|
|
168
168
|
if (keys.has(denyKey('subject', '*', key))) return true
|
|
@@ -200,9 +200,7 @@ export function addDenylistEntry(entry) {
|
|
|
200
200
|
throw new Error('scope and value required')
|
|
201
201
|
if (scope === 'entity' && entry.groupId)
|
|
202
202
|
throw new Error('entity scope does not use groupId')
|
|
203
|
-
const normValue =
|
|
204
|
-
? normalizeHex64(entry.value)
|
|
205
|
-
: entry.value
|
|
203
|
+
const normValue = entry.value
|
|
206
204
|
if (!normValue)
|
|
207
205
|
throw new Error('scope and value required')
|
|
208
206
|
if (scope === 'subject' && !isHex64(normValue))
|
|
@@ -234,8 +232,8 @@ export async function addDenylistFromBanContent(banContent, groupId) {
|
|
|
234
232
|
await addDenylistEntry({ scope: 'entity', value: banContent.targetEntityHash })
|
|
235
233
|
if (scope === 'node' && banContent?.targetNodeHash)
|
|
236
234
|
await addDenylistEntry({ scope: 'node', value: banContent.targetNodeHash })
|
|
237
|
-
const pk =
|
|
238
|
-
if (
|
|
235
|
+
const pk = isHex64(banContent?.targetPubKeyHash)
|
|
236
|
+
if (pk)
|
|
239
237
|
await addDenylistEntry({ scope: 'subject', value: pk, ...sourceGroupId ? { groupId: sourceGroupId } : {} })
|
|
240
238
|
}
|
|
241
239
|
|
|
@@ -259,12 +257,11 @@ export function addGroupBlockedPeer(groupId, scope, value) {
|
|
|
259
257
|
* @returns {Promise<void>}
|
|
260
258
|
*/
|
|
261
259
|
export function removeGroupBlockedPeer(groupId, scope, value) {
|
|
262
|
-
|
|
263
|
-
if (!scope || !id) return Promise.resolve()
|
|
260
|
+
if (!scope || !value) return Promise.resolve()
|
|
264
261
|
return mutateDenylist(() => {
|
|
265
262
|
const list = loadDenylist()
|
|
266
263
|
list.blocked = list.blocked.filter(entry => {
|
|
267
|
-
if (entry.scope !== scope || entry.value !==
|
|
264
|
+
if (entry.scope !== scope || entry.value !== value) return true
|
|
268
265
|
if (scope === 'entity') return false
|
|
269
266
|
return entry.groupId !== groupId
|
|
270
267
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用包级行为开关(布尔 feature map):`{ census: true, ... }`。
|
|
3
|
+
* 新增功能只需加入新布尔 key,无需新增专属设置界面。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 默认 feature:census 默认开启(人口统计,基于 nostr)。 */
|
|
7
|
+
const DEFAULT_P2P_FEATURES = { census: true }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @returns {Record<string, boolean>} 默认 feature map 快照
|
|
11
|
+
*/
|
|
12
|
+
export function defaultP2PFeatures() {
|
|
13
|
+
return { ...DEFAULT_P2P_FEATURES }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 归一化 feature map:已知 key 用默认值,patch 中未知 key 原样透传(值必须为 boolean)。
|
|
18
|
+
* @param {Record<string, boolean>} [patch] 部分 feature 覆盖
|
|
19
|
+
* @returns {Record<string, boolean>} 完整 boolean feature map
|
|
20
|
+
*/
|
|
21
|
+
export function resolveP2PFeatures(patch = {}) {
|
|
22
|
+
/** @type {Record<string, boolean>} */
|
|
23
|
+
const out = defaultP2PFeatures()
|
|
24
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
25
|
+
if (value !== true && value !== false)
|
|
26
|
+
throw new Error(`p2p: feature "${key}" must be boolean`)
|
|
27
|
+
out[key] = value
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
}
|
package/node/identity.mjs
CHANGED
|
@@ -2,7 +2,7 @@ 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
|
|
5
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
6
6
|
import { keyPairFromSeed, pubKeyHash } from '../crypto/crypto.mjs'
|
|
7
7
|
import { normalizeMailboxSettings } from '../mailbox/settings.mjs'
|
|
8
8
|
|
|
@@ -17,7 +17,7 @@ const NODE_JSON = 'node'
|
|
|
17
17
|
* @returns {string} 节点哈希
|
|
18
18
|
*/
|
|
19
19
|
export function nodeHashFromSeed(seedHex) {
|
|
20
|
-
const seed = Buffer.from(
|
|
20
|
+
const seed = Buffer.from(seedHex, 'hex')
|
|
21
21
|
if (seed.length !== 32) throw new Error('invalid node seed')
|
|
22
22
|
const { publicKey } = keyPairFromSeed(seed)
|
|
23
23
|
return pubKeyHash(publicKey)
|
|
@@ -46,8 +46,8 @@ function saveNodeFile(patch) {
|
|
|
46
46
|
*/
|
|
47
47
|
export function ensureNodeSeed() {
|
|
48
48
|
const data = loadNodeFile()
|
|
49
|
-
const existing =
|
|
50
|
-
if (
|
|
49
|
+
const existing = isHex64(data.nodeSeedHex)
|
|
50
|
+
if (existing) return existing
|
|
51
51
|
const nodeSeedHex = randomBytes(32).toString('hex')
|
|
52
52
|
saveNodeFile({ nodeSeedHex })
|
|
53
53
|
return nodeSeedHex
|
|
@@ -105,8 +105,8 @@ export function ensureNodeDefaults() {
|
|
|
105
105
|
* @returns {string | null} entityHash(非法 hex 时 null)
|
|
106
106
|
*/
|
|
107
107
|
export function entityHashFromKeys(nodeHash, recoveryPubKeyHex) {
|
|
108
|
-
const pub =
|
|
109
|
-
if (!isHex64(nodeHash) || !
|
|
108
|
+
const pub = isHex64(recoveryPubKeyHex)
|
|
109
|
+
if (!isHex64(nodeHash) || !pub) return null
|
|
110
110
|
return entityHashFromRecoveryPubKeyHex(nodeHash, pub)
|
|
111
111
|
}
|
|
112
112
|
|
package/node/instance.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
3
|
import { createFsEntityStore } from './entity_store.mjs'
|
|
4
|
+
import { resolveP2PFeatures } from './feature_config.mjs'
|
|
4
5
|
import { closeAllFileStreams } from './handles.mjs'
|
|
5
6
|
import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './signaling_config.mjs'
|
|
6
7
|
|
|
@@ -14,6 +15,7 @@ import { defaultSignalingRuntimeConfig, resolveSignalingRuntimeConfig } from './
|
|
|
14
15
|
* entityStore: import('./entity_store.mjs').EntityStore
|
|
15
16
|
* logger: NodeLogger | null
|
|
16
17
|
* signaling: SignalingRuntimeConfig
|
|
18
|
+
* features: Record<string, boolean>
|
|
17
19
|
* }} NodeRuntime
|
|
18
20
|
*/
|
|
19
21
|
|
|
@@ -49,6 +51,7 @@ export function initNode(options) {
|
|
|
49
51
|
entityStore,
|
|
50
52
|
logger: console,
|
|
51
53
|
signaling: resolveSignalingRuntimeConfig(),
|
|
54
|
+
features: resolveP2PFeatures(),
|
|
52
55
|
}
|
|
53
56
|
return runtime
|
|
54
57
|
}
|
|
@@ -101,6 +104,24 @@ export function getSignalingRuntimeConfig() {
|
|
|
101
104
|
return runtime?.signaling ?? defaultSignalingRuntimeConfig()
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 设置包级行为开关(布尔 feature map);运行时切换,生效于对应 feature 的下一轮。
|
|
109
|
+
* @param {Record<string, boolean>} config - feature 补丁
|
|
110
|
+
* @returns {void}
|
|
111
|
+
*/
|
|
112
|
+
export function setP2PFeatures(config) {
|
|
113
|
+
if (!runtime) throw new Error('p2p: setP2PFeatures requires initNode')
|
|
114
|
+
runtime.features = resolveP2PFeatures({ ...runtime.features, ...config })
|
|
115
|
+
emitNodeChange('features-changed', runtime.features)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @returns {Record<string, boolean>} 当前包级 feature map
|
|
120
|
+
*/
|
|
121
|
+
export function getP2PFeatures() {
|
|
122
|
+
return { ...getNode().features }
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
/**
|
|
105
126
|
* @returns {string} 节点数据目录绝对路径
|
|
106
127
|
*/
|
package/node/network.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
2
|
-
import { isHex64
|
|
2
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
3
3
|
|
|
4
4
|
import { loadDenylist } from './denylist.mjs'
|
|
5
5
|
import { getNodeDir, isNodeInitialized } from './instance.mjs'
|
|
@@ -57,7 +57,7 @@ export function normalizeNetwork(raw) {
|
|
|
57
57
|
)]
|
|
58
58
|
const hints = (file.hints || [])
|
|
59
59
|
.map(hint => ({
|
|
60
|
-
nodeHash:
|
|
60
|
+
nodeHash: hint.nodeHash || '',
|
|
61
61
|
source: String(hint.source || ''),
|
|
62
62
|
kind: String(hint.kind || ''),
|
|
63
63
|
weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
|
|
@@ -126,8 +126,8 @@ export function saveNetwork(data) {
|
|
|
126
126
|
* @returns {void}
|
|
127
127
|
*/
|
|
128
128
|
export function applyNetworkHint(hint) {
|
|
129
|
-
const nodeHash =
|
|
130
|
-
if (!
|
|
129
|
+
const nodeHash = isHex64(hint?.nodeHash)
|
|
130
|
+
if (!nodeHash) return
|
|
131
131
|
const net = loadNetwork()
|
|
132
132
|
const now = Date.now()
|
|
133
133
|
const ttlMs = Number.isFinite(hint.ttlMs) ? hint.ttlMs : DEFAULT_EXPLORE_TTL_MS
|
|
@@ -160,8 +160,8 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
160
160
|
const net = loadNetwork()
|
|
161
161
|
const now = Date.now()
|
|
162
162
|
for (const raw of net.trustedPeers.slice(0, 12)) {
|
|
163
|
-
const nodeHash =
|
|
164
|
-
if (!
|
|
163
|
+
const nodeHash = isHex64(raw)
|
|
164
|
+
if (!nodeHash) continue
|
|
165
165
|
if (!net.explorePeers.includes(nodeHash))
|
|
166
166
|
net.explorePeers.push(nodeHash)
|
|
167
167
|
net.hints.push({
|
|
@@ -185,12 +185,12 @@ export function widenExploreFromTrustedAnchors() {
|
|
|
185
185
|
export function mergeNetworkPeerPools(patch = {}) {
|
|
186
186
|
const net = loadNetwork()
|
|
187
187
|
for (const raw of patch.trustedPeers || []) {
|
|
188
|
-
const id =
|
|
189
|
-
if (
|
|
188
|
+
const id = isHex64(raw)
|
|
189
|
+
if (id && !net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
190
190
|
}
|
|
191
191
|
for (const raw of patch.explorePeers || []) {
|
|
192
|
-
const id =
|
|
193
|
-
if (
|
|
192
|
+
const id = isHex64(raw)
|
|
193
|
+
if (id && !net.explorePeers.includes(id)) net.explorePeers.push(id)
|
|
194
194
|
}
|
|
195
195
|
net.lastRosterAt = Date.now()
|
|
196
196
|
saveNetwork(net)
|
|
@@ -203,8 +203,8 @@ export function mergeNetworkPeerPools(patch = {}) {
|
|
|
203
203
|
*/
|
|
204
204
|
export function promoteExplorePeer(nodeHash) {
|
|
205
205
|
const net = loadNetwork()
|
|
206
|
-
const id =
|
|
207
|
-
if (!
|
|
206
|
+
const id = isHex64(nodeHash)
|
|
207
|
+
if (!id) return
|
|
208
208
|
net.explorePeers = net.explorePeers.filter(peer => peer !== id)
|
|
209
209
|
if (!net.trustedPeers.includes(id)) net.trustedPeers.push(id)
|
|
210
210
|
net.lastRosterAt = Date.now()
|
package/node/personal_block.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* **hide**:纯本地隐藏(personal_hide.json,永不联邦)。
|
|
5
5
|
*/
|
|
6
6
|
import { parseEntityHash } from '../core/entity_id.mjs'
|
|
7
|
-
import { isHex64
|
|
7
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
8
8
|
|
|
9
9
|
import { isWritableLocalEntity } from './identity.mjs'
|
|
10
10
|
import { isNodeInitialized, getEntityStore } from './instance.mjs'
|
|
@@ -45,8 +45,8 @@ export function normalizePersonalListEntries(raw) {
|
|
|
45
45
|
byKey.set(`entity:${parsed.entityHash}`, { scope: 'entity', value: parsed.entityHash })
|
|
46
46
|
}
|
|
47
47
|
else if (scope === 'subject') {
|
|
48
|
-
const value =
|
|
49
|
-
if (
|
|
48
|
+
const value = isHex64(entry?.value)
|
|
49
|
+
if (value)
|
|
50
50
|
byKey.set(`subject:${value}`, { scope: 'subject', value })
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -64,7 +64,7 @@ export function normalizePersonalListEntries(raw) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function matchesPersonalListEntries(entries, subject) {
|
|
66
66
|
const entity = subject?.entityHash || ''
|
|
67
|
-
const pk =
|
|
67
|
+
const pk = subject?.pubKeyHash || subject?.subjectHash || ''
|
|
68
68
|
if (entity) {
|
|
69
69
|
for (const entry of entries)
|
|
70
70
|
if (entry.scope === 'entity' && entry.value === entity) return true
|