@steve02081504/fount-p2p 0.0.36 → 0.0.38
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 +3 -3
- package/discovery/adverts.mjs +16 -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/docs/evfs.md +15 -6
- package/docs/nostr_relay_discovery.md +92 -0
- package/files/chunk/responder.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/files/fed/fetch_shared.mjs +3 -1
- package/files/fed/responder.mjs +3 -2
- package/files/fetch_fanout.mjs +26 -5
- package/files/manifest/fetch.mjs +73 -38
- package/files/manifest/pending.mjs +21 -7
- package/files/manifest/servicer_registry.mjs +51 -0
- package/governance/branch.mjs +1 -1
- package/governance/join_pow.mjs +2 -2
- package/index.mjs +11 -4
- package/link/handshake.mjs +135 -23
- package/link/providers/link_id_pipe.mjs +1 -1
- package/link/providers/{nostr.mjs → nostr/index.mjs} +19 -14
- package/mailbox/deliver_or_store.mjs +3 -4
- package/mailbox/store.mjs +1 -1
- package/node/feature_config.mjs +30 -0
- package/node/instance.mjs +21 -0
- package/node/reputation_store.mjs +1 -2
- 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 +1 -2
- package/transport/runtime_bootstrap.mjs +39 -3
- package/utils/fetch_wait.mjs +12 -7
- package/discovery/nostr.mjs +0 -1073
package/link/handshake.mjs
CHANGED
|
@@ -5,7 +5,10 @@ import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
|
5
5
|
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
6
6
|
import { keyPairFromSeed, pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
7
7
|
import { normalizeLanHosts } from '../discovery/lan_interfaces.mjs'
|
|
8
|
+
import { MAX_ADVERT_LISTEN_RELAYS, MAX_ADVERT_RELAY_POOL, MAX_RTT_MS } from '../discovery/nostr/constants.mjs'
|
|
9
|
+
import { normalizeNostrRelayUrl } from '../discovery/nostr/relays.mjs'
|
|
8
10
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
11
|
+
import { nodeDebug } from '../node/log.mjs'
|
|
9
12
|
|
|
10
13
|
import { normalizeDtlsFingerprint } from './sdp_fingerprint.mjs'
|
|
11
14
|
|
|
@@ -20,9 +23,7 @@ export const LINK_HANDSHAKE_DOMAIN = 'fount-link'
|
|
|
20
23
|
* @returns {string | null} 规范化 binding,无效时 null
|
|
21
24
|
*/
|
|
22
25
|
export function normalizeLinkBinding(value) {
|
|
23
|
-
|
|
24
|
-
if (dtls) return dtls
|
|
25
|
-
return isHex64(value)
|
|
26
|
+
return normalizeDtlsFingerprint(value) || isHex64(value)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -33,16 +34,14 @@ export function normalizeLinkBinding(value) {
|
|
|
33
34
|
* @returns {Uint8Array} 待签名消息字节
|
|
34
35
|
*/
|
|
35
36
|
export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
|
|
36
|
-
const nonce = peerNonce
|
|
37
37
|
const binding = normalizeLinkBinding(localBinding)
|
|
38
|
-
|
|
39
|
-
if (!/^[\da-f]{64}$/u.test(nonce))
|
|
38
|
+
if (!/^[\da-f]{64}$/u.test(peerNonce))
|
|
40
39
|
throw new Error('p2p: auth nonce must be 64 hex characters')
|
|
41
40
|
if (!binding)
|
|
42
41
|
throw new Error('p2p: link binding missing or invalid')
|
|
43
|
-
if (!isHex64(
|
|
42
|
+
if (!isHex64(localNodeHash))
|
|
44
43
|
throw new Error('p2p: nodeHash must be 64 hex characters')
|
|
45
|
-
return Buffer.from(`${LINK_HANDSHAKE_DOMAIN}\0${
|
|
44
|
+
return Buffer.from(`${LINK_HANDSHAKE_DOMAIN}\0${peerNonce}\0${binding}\0${localNodeHash}`, 'utf8')
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
/**
|
|
@@ -137,23 +136,130 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
|
137
136
|
* @param {string} nodeHash 节点 nodeHash
|
|
138
137
|
* @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
|
|
139
138
|
* @param {unknown} [lanHosts=null] 可选 LAN IPv4 列表(签入消息)
|
|
139
|
+
* @param {string | null} [relayBlobHex=null] 可选规范化 relay 字段 hex 段(签入消息)
|
|
140
140
|
* @returns {Uint8Array} 待签名消息字节
|
|
141
141
|
*/
|
|
142
|
-
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null) {
|
|
143
|
-
|
|
142
|
+
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null, relayBlobHex = null) {
|
|
143
|
+
let message = `fount-advert\0${rendezvousKey}\0${ts}\0${nodeHash}`
|
|
144
144
|
const port = normalizeTcpPort(tcpPort)
|
|
145
|
-
|
|
145
|
+
if (port) message += `\0${port}`
|
|
146
146
|
const hosts = normalizeLanHosts(lanHosts)
|
|
147
147
|
if (hosts.length) message += `\0${hosts.join(',')}`
|
|
148
|
+
if (relayBlobHex) message += `\0relays:${relayBlobHex}`
|
|
148
149
|
return Buffer.from(message, 'utf8')
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
|
-
*
|
|
153
|
+
* 规范化并裁剪入站(不可信)advert 携带的 relay 字段:无效项丢弃并记审计日志,不抛错。
|
|
154
|
+
* - pool:url 经 normalize 有效且 rttMs∈[0,MAX_RTT_MS],取整、去重、裁剪前 MAX_ADVERT_RELAY_POOL。
|
|
155
|
+
* - listen:url normalize 有效、去重、裁剪前 MAX_ADVERT_LISTEN_RELAYS。
|
|
156
|
+
* - 每个丢弃项记审计日志(含原始值),不静默。
|
|
157
|
+
* @param {unknown} rawPool 原始 pool([{url, rttMs}])
|
|
158
|
+
* @param {unknown} rawListen 原始 listen([url])
|
|
159
|
+
* @returns {{ pool: Array<{ url: string, rtt: number }>, listen: string[] }} 规范化结果
|
|
160
|
+
*/
|
|
161
|
+
export function sanitizeAdvertRelayFields(rawPool, rawListen) {
|
|
162
|
+
/** @type {Array<{ url: string, rtt: number }>} */
|
|
163
|
+
const pool = []
|
|
164
|
+
/** @type {Set<string>} */
|
|
165
|
+
const seenPool = new Set()
|
|
166
|
+
if (Array.isArray(rawPool)) for (const item of rawPool) {
|
|
167
|
+
const url = normalizeNostrRelayUrl(item?.url)
|
|
168
|
+
if (!url) {
|
|
169
|
+
nodeDebug('invalidRelayUrl', { url: String(item?.url), reason: 'advert-pool-invalid-url' })
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
const rtt = Number(item?.rttMs ?? item?.rtt)
|
|
173
|
+
if (!Number.isFinite(rtt) || rtt < 0 || rtt > MAX_RTT_MS) {
|
|
174
|
+
nodeDebug('invalidRelayUrl', { url, reason: 'advert-pool-invalid-rtt', rttMs: item?.rttMs ?? item?.rtt })
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (seenPool.has(url)) continue
|
|
178
|
+
seenPool.add(url)
|
|
179
|
+
pool.push({ url, rtt: Math.round(rtt) })
|
|
180
|
+
if (pool.length >= MAX_ADVERT_RELAY_POOL) break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** @type {string[]} */
|
|
184
|
+
const listen = []
|
|
185
|
+
/** @type {Set<string>} */
|
|
186
|
+
const seenListen = new Set()
|
|
187
|
+
if (Array.isArray(rawListen)) for (const raw of rawListen) {
|
|
188
|
+
const url = normalizeNostrRelayUrl(raw)
|
|
189
|
+
if (!url) {
|
|
190
|
+
nodeDebug('invalidRelayUrl', { url: String(raw), reason: 'advert-listen-invalid-url' })
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (seenListen.has(url)) continue
|
|
194
|
+
seenListen.add(url)
|
|
195
|
+
listen.push(url)
|
|
196
|
+
if (listen.length >= MAX_ADVERT_LISTEN_RELAYS) break
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { pool, listen }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 构建规范化 relay 字段的 canonical blob(pool/listen 各按 url 排序 → `{p,l}` JSON → UTF-8 → hex)。
|
|
204
|
+
* @param {Array<{ url: string, rtt: number }>} pool 规范化 pool
|
|
205
|
+
* @param {string[]} listen 规范化 listen
|
|
206
|
+
* @returns {string} hex 编码 blob
|
|
207
|
+
*/
|
|
208
|
+
export function canonicalAdvertRelayBlob(pool, listen) {
|
|
209
|
+
return Buffer.from(JSON.stringify({
|
|
210
|
+
p: [...pool].sort((a, b) => a.url < b.url ? -1 : a.url > b.url ? 1 : 0),
|
|
211
|
+
l: [...listen].sort(),
|
|
212
|
+
}), 'utf8').toString('hex')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 严格规范化本机(出站)提供的 relay 字段:无效数据立即抛错,不静默丢弃。
|
|
217
|
+
* 本地调用方应提供已规范化字段;入站校验仍走 lenient 的 sanitizeAdvertRelayFields。
|
|
218
|
+
* @param {unknown} rawPool 原始 pool([{url, rttMs}])
|
|
219
|
+
* @param {unknown} rawListen 原始 listen([url])
|
|
220
|
+
* @returns {{ pool: Array<{ url: string, rtt: number }>, listen: string[] }} 规范化结果
|
|
221
|
+
*/
|
|
222
|
+
function normalizeLocalRelayFields(rawPool, rawListen) {
|
|
223
|
+
/** @type {Array<{ url: string, rtt: number }>} */
|
|
224
|
+
const pool = []
|
|
225
|
+
/** @type {Set<string>} */
|
|
226
|
+
const seenPool = new Set()
|
|
227
|
+
if (rawPool != null && !Array.isArray(rawPool)) throw new Error('p2p: advert pool invalid type')
|
|
228
|
+
if (Array.isArray(rawPool)) for (const item of rawPool) {
|
|
229
|
+
const url = normalizeNostrRelayUrl(item?.url)
|
|
230
|
+
if (!url) throw new Error('p2p: advert pool invalid url')
|
|
231
|
+
const rtt = Number(item?.rttMs ?? item?.rtt)
|
|
232
|
+
if (!Number.isFinite(rtt) || rtt < 0 || rtt > MAX_RTT_MS)
|
|
233
|
+
throw new Error('p2p: advert pool invalid rtt')
|
|
234
|
+
if (seenPool.has(url)) continue
|
|
235
|
+
seenPool.add(url)
|
|
236
|
+
pool.push({ url, rtt: Math.round(rtt) })
|
|
237
|
+
if (pool.length >= MAX_ADVERT_RELAY_POOL) break
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @type {string[]} */
|
|
241
|
+
const listen = []
|
|
242
|
+
/** @type {Set<string>} */
|
|
243
|
+
const seenListen = new Set()
|
|
244
|
+
if (rawListen != null && !Array.isArray(rawListen)) throw new Error('p2p: advert listen invalid type')
|
|
245
|
+
if (Array.isArray(rawListen)) for (const raw of rawListen) {
|
|
246
|
+
const url = normalizeNostrRelayUrl(raw)
|
|
247
|
+
if (!url) throw new Error('p2p: advert listen invalid url')
|
|
248
|
+
if (seenListen.has(url)) continue
|
|
249
|
+
seenListen.add(url)
|
|
250
|
+
listen.push(url)
|
|
251
|
+
if (listen.length >= MAX_ADVERT_LISTEN_RELAYS) break
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { pool, listen }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 构造带签名的 discovery advert(tcpPort / lanHosts / relay 字段一并签入消息)。
|
|
153
259
|
* @param {string} rendezvousKey discovery 内部汇合键
|
|
154
260
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
155
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number, lanHosts?: unknown } | null} [options] 签名身份;可选 LAN
|
|
156
|
-
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number, lanHosts?: string[] }>} 签名 advert
|
|
261
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number, lanHosts?: unknown, nostrRelayPool?: unknown, listenNostrRelays?: unknown } | null} [options] 签名身份;可选 LAN 监听端口、本机 IPv4 列表与 relay 字段
|
|
262
|
+
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number, lanHosts?: string[], nostrRelayPool?: Array<{ url: string, rtt: number }>, listenNostrRelays?: string[] }>} 签名 advert
|
|
157
263
|
*/
|
|
158
264
|
export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options = null) {
|
|
159
265
|
const seed = options?.secretKey
|
|
@@ -168,7 +274,10 @@ export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options
|
|
|
168
274
|
if (options?.tcpPort && !tcpPort)
|
|
169
275
|
throw new Error('p2p: advert tcpPort invalid')
|
|
170
276
|
const lanHosts = normalizeLanHosts(options?.lanHosts)
|
|
171
|
-
|
|
277
|
+
// 出站字段要求已规范化,无效数据直接抛错(入站路径才用 lenient sanitize)。
|
|
278
|
+
const normalized = normalizeLocalRelayFields(options?.nostrRelayPool, options?.listenNostrRelays)
|
|
279
|
+
const message = buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort, lanHosts,
|
|
280
|
+
canonicalAdvertRelayBlob(normalized.pool, normalized.listen))
|
|
172
281
|
const sig = await sign(message, secretKey)
|
|
173
282
|
const advert = {
|
|
174
283
|
nodeHash,
|
|
@@ -178,16 +287,18 @@ export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options
|
|
|
178
287
|
}
|
|
179
288
|
if (tcpPort) advert.tcpPort = tcpPort
|
|
180
289
|
if (lanHosts.length) advert.lanHosts = lanHosts
|
|
290
|
+
if (normalized.pool.length) advert.nostrRelayPool = normalized.pool
|
|
291
|
+
if (normalized.listen.length) advert.listenNostrRelays = normalized.listen
|
|
181
292
|
return advert
|
|
182
293
|
}
|
|
183
294
|
|
|
184
295
|
/**
|
|
185
|
-
* 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash
|
|
296
|
+
* 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash 与规范化后的 relay 字段。
|
|
186
297
|
* @param {string} rendezvousKey 期望的汇合键
|
|
187
298
|
* @param {unknown} advert 原始 advert 载荷
|
|
188
299
|
* @param {number} [now=Date.now()] 当前时间(毫秒)
|
|
189
300
|
* @param {number} [maxSkewMs=10 * 60_000] 允许的最大时钟偏差(毫秒)
|
|
190
|
-
* @returns {Promise<string | null>}
|
|
301
|
+
* @returns {Promise<{ nodeHash: string, relayPool: Array<{ url: string, rtt: number }>, listenRelays: string[] } | null>} 验证通过返回 nodeHash 与规范化 relay 字段,失败返回 null
|
|
191
302
|
*/
|
|
192
303
|
export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
|
|
193
304
|
const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
|
|
@@ -195,11 +306,12 @@ export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now()
|
|
|
195
306
|
const ts = Number(advert?.ts)
|
|
196
307
|
const sig = isSignatureHex128(advert?.sig)
|
|
197
308
|
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !sig) return null
|
|
198
|
-
const hasTcpPortField = !!advert?.tcpPort
|
|
199
309
|
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
200
|
-
if (
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
310
|
+
if (advert?.tcpPort != null && !tcpPort) return null
|
|
311
|
+
const sanitized = sanitizeAdvertRelayFields(advert?.nostrRelayPool, advert?.listenNostrRelays)
|
|
312
|
+
if (!await verify(Buffer.from(sig, 'hex'),
|
|
313
|
+
buildAdvertMessage(rendezvousKey, ts, parsedHello.nodeHash, tcpPort, normalizeLanHosts(advert?.lanHosts),
|
|
314
|
+
canonicalAdvertRelayBlob(sanitized.pool, sanitized.listen)),
|
|
315
|
+
Buffer.from(parsedHello.nodePubKey, 'hex'))) return null
|
|
316
|
+
return { nodeHash: parsedHello.nodeHash, relayPool: sanitized.pool, listenRelays: sanitized.listen }
|
|
205
317
|
}
|
|
@@ -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 = options
|
|
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 { isHex64 } from '
|
|
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,8 +316,7 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
317
316
|
*/
|
|
318
317
|
async function deliverPacket(packet) {
|
|
319
318
|
if (packet?.type !== 'link') return
|
|
320
|
-
const linkId = packet
|
|
321
|
-
const from = packet.from
|
|
319
|
+
const { linkId, from } = packet
|
|
322
320
|
if (!isHex64(linkId) || !isHex64(from)) return
|
|
323
321
|
if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
|
|
324
322
|
|
|
@@ -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')
|
|
@@ -62,7 +62,7 @@ async function resolveRouting(username) {
|
|
|
62
62
|
*/
|
|
63
63
|
export async function deliverOrStoreMailboxPut(username, options) {
|
|
64
64
|
const routing = await resolveRouting(username)
|
|
65
|
-
const toPubKeyHash = options
|
|
65
|
+
const { toPubKeyHash } = options
|
|
66
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 }
|
|
@@ -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
|
package/mailbox/store.mjs
CHANGED
|
@@ -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 = record
|
|
153
|
+
const { toPubKeyHash } = record
|
|
154
154
|
if (!isHex64(toPubKeyHash)) return false
|
|
155
155
|
const hop = normalizeMailboxHop(record.hop)
|
|
156
156
|
const { tier } = record
|
|
@@ -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/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
|
*/
|
|
@@ -228,12 +228,11 @@ export async function applySubjectiveSlashFromEvent(username, groupId, event, re
|
|
|
228
228
|
if (event.type !== 'reputation_slash') return
|
|
229
229
|
const { content } = event
|
|
230
230
|
const target = content.targetPubKeyHash
|
|
231
|
-
const sender = event.sender
|
|
232
231
|
|
|
233
232
|
await mutateReputation(async data => {
|
|
234
233
|
const verified = content.verified && await verifySlashProof(username, groupId, content, readEvents)
|
|
235
234
|
const claim = Number(content.claim ?? (verified ? reputationTunables.slashVerifiedDefaultClaim : reputationTunables.slashUnverifiedDefaultClaim))
|
|
236
|
-
applySubjectiveSlashPure(data, target, sender, claim, verified)
|
|
235
|
+
applySubjectiveSlashPure(data, target, event.sender, claim, verified)
|
|
237
236
|
})
|
|
238
237
|
}
|
|
239
238
|
|
package/node/storage.mjs
CHANGED
|
@@ -28,3 +28,26 @@ export function readNodeJsonSync(name) {
|
|
|
28
28
|
export function writeNodeJsonSync(name, data) {
|
|
29
29
|
writeJsonFileSync(nodeJsonPath(name), data)
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Nostr relay 池 / peer 路由持久化文件路径(`nodeDir/nostr/relays.json`)。
|
|
34
|
+
* @returns {string} 绝对路径
|
|
35
|
+
*/
|
|
36
|
+
export function nostrRelayJsonPath() {
|
|
37
|
+
return path.join(getNodeDir(), 'nostr', 'relays.json')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @returns {object | null} 解析后的 Nostr relay JSON 或 null
|
|
42
|
+
*/
|
|
43
|
+
export function readNostrRelaysJsonSync() {
|
|
44
|
+
return readJsonFileSync(nostrRelayJsonPath())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} data 数据
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
export function writeNostrRelaysJsonSync(data) {
|
|
52
|
+
writeJsonFileSync(nostrRelayJsonPath(), data)
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"node": ">=20"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
|
-
"test": "node --test --test-concurrency=1 --test-force-exit test/pure/*.test.mjs test/integration/*.test.mjs",
|
|
24
|
+
"test": "node --test --test-concurrency=1 --test-force-exit test/pure/*.test.mjs test/integration/*.test.mjs test/frontend/*.test.mjs",
|
|
25
25
|
"test:fount": "deno test -A --config deno.json test/fount/",
|
|
26
26
|
"test:live": "node --test --test-concurrency=1 --test-force-exit test/live/*.test.mjs",
|
|
27
|
-
"test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs"
|
|
27
|
+
"test:sim": "node --test --test-concurrency=1 --test-force-exit sim/test/*.test.mjs",
|
|
28
|
+
"test:frontend": "node --test --test-concurrency=1 --test-force-exit test/frontend/*.test.mjs"
|
|
28
29
|
},
|
|
29
30
|
"bin": {
|
|
30
31
|
"fount-p2p": "infra/cli.mjs"
|
|
@@ -33,6 +34,8 @@
|
|
|
33
34
|
".": "./index.mjs",
|
|
34
35
|
"./infra": "./infra/service.mjs",
|
|
35
36
|
"./link": "./link/providers/index.mjs",
|
|
37
|
+
"./link/providers/nostr": "./link/providers/nostr/index.mjs",
|
|
38
|
+
"./link/providers/nostr/*": "./link/providers/nostr/*.mjs",
|
|
36
39
|
"./core/*": "./core/*.mjs",
|
|
37
40
|
"./crypto": "./crypto/crypto.mjs",
|
|
38
41
|
"./crypto/*": "./crypto/*.mjs",
|
|
@@ -43,6 +46,8 @@
|
|
|
43
46
|
"./discovery": "./discovery/index.mjs",
|
|
44
47
|
"./discovery/bt": "./discovery/bt/index.mjs",
|
|
45
48
|
"./discovery/bt/*": "./discovery/bt/*.mjs",
|
|
49
|
+
"./discovery/nostr": "./discovery/nostr/index.mjs",
|
|
50
|
+
"./discovery/nostr/*": "./discovery/nostr/*.mjs",
|
|
46
51
|
"./discovery/*": "./discovery/*.mjs",
|
|
47
52
|
"./transport/node_scope/*": "./transport/node_scope/*.mjs",
|
|
48
53
|
"./transport/*": "./transport/*.mjs",
|
|
@@ -73,7 +78,8 @@
|
|
|
73
78
|
"ws": "latest"
|
|
74
79
|
},
|
|
75
80
|
"devDependencies": {
|
|
76
|
-
"@steve02081504/exec": "latest"
|
|
81
|
+
"@steve02081504/exec": "latest",
|
|
82
|
+
"playwright-core": "latest"
|
|
77
83
|
},
|
|
78
84
|
"optionalDependencies": {
|
|
79
85
|
"@stoprocent/bleno": "latest",
|