@steve02081504/fount-p2p 0.0.33 → 0.0.35

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/core/hexIds.mjs CHANGED
@@ -20,10 +20,11 @@ export function normalizeHex64(value) {
20
20
 
21
21
  /**
22
22
  * @param {unknown} value 待校验值
23
- * @returns {boolean} 是否为 64 位小写 hex
23
+ * @returns {string | null} 合法时返回规范化 64 hex,否则 null
24
24
  */
25
25
  export function isHex64(value) {
26
- return HEX_ID_64.test(normalizeHex64(value))
26
+ const normalized = normalizeHex64(value)
27
+ return HEX_ID_64.test(normalized) ? normalized : null
27
28
  }
28
29
 
29
30
  /**
@@ -53,8 +54,9 @@ export function assertHex64(value, label = 'hex64') {
53
54
 
54
55
  /**
55
56
  * @param {unknown} value 待校验值
56
- * @returns {boolean} 是否为 128 位签名 hex
57
+ * @returns {string | null} 合法时返回 128 位签名 hex,否则 null
57
58
  */
58
59
  export function isSignatureHex128(value) {
59
- return SIGNATURE_HEX_128.test(String(value ?? ''))
60
+ const normalized = String(value ?? '')
61
+ return SIGNATURE_HEX_128.test(normalized) ? normalized : null
60
62
  }
@@ -41,7 +41,7 @@ export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
41
41
  * @returns {boolean} 签名格式合法为 true
42
42
  */
43
43
  export function isSignedCheckpoint(checkpoint) {
44
- return isSignatureHex128(String(checkpoint?.checkpoint_signature || ''))
44
+ return !!isSignatureHex128(String(checkpoint?.checkpoint_signature || ''))
45
45
  }
46
46
 
47
47
  /**
package/discovery/lan.mjs CHANGED
@@ -95,7 +95,7 @@ export function createLanDiscoveryProvider(options = {}) {
95
95
  let beaconTimer = null
96
96
  const seededSelf = normalizeHex64(options.localNodeHash)
97
97
  /** @type {string | null} */
98
- let selfNodeHash = isHex64(seededSelf) ? seededSelf : null
98
+ let selfNodeHash = isHex64(seededSelf)
99
99
  /** @type {Set<string>} */
100
100
  const joinedAddresses = new Set()
101
101
 
@@ -25,6 +25,10 @@ export const DEFAULT_RELAY_URLS = [
25
25
  'wss://relay.damus.io',
26
26
  'wss://nos.lol',
27
27
  'wss://relay.nostr.band',
28
+ 'wss://relay.nostr.com',
29
+ 'wss://nostr.bitcoiner.social',
30
+ 'wss://nostr.mom',
31
+ 'wss://relay.snort.social',
28
32
  ]
29
33
 
30
34
  /** 单中继 WebSocket 首连超时(短超时 + 并行,避免串行 10s×N)。 */
@@ -44,6 +48,9 @@ export const NOSTR_ADVERT_KIND = 30787
44
48
  /** Nostr signal 事件 kind(ephemeral,实时转发)。 */
45
49
  export const NOSTR_SIGNAL_KIND = 20787
46
50
 
51
+ /** 打广告用的话题 tag(hashtag,NIP-01),公开可被搜索聚合。 */
52
+ const NOSTR_TOPIC_TAG = ['t', 'fount']
53
+
47
54
  const ADVERT_TTL_MS = 10 * 60_000
48
55
 
49
56
  /** @type {Map<string, number>} 网络域 nodeHash → lastSeenAt */
@@ -756,7 +763,7 @@ export function createNostrDiscoveryProvider(options = {}) {
756
763
  const secretKey = randomBytes(32)
757
764
  const seededSelf = normalizeHex64(options.localNodeHash)
758
765
  /** @type {string | null} */
759
- let selfNodeHash = isHex64(seededSelf) ? seededSelf : null
766
+ let selfNodeHash = isHex64(seededSelf)
760
767
  const NETWORK_SUB_KEY = 'network'
761
768
  /**
762
769
  * @typedef {{ stop: () => void, held: boolean, listeners: Set<(bytes: Uint8Array, meta: object) => void> }} AdvertSubEntry
@@ -925,7 +932,7 @@ export function createNostrDiscoveryProvider(options = {}) {
925
932
  const bytes = encryptSignalPacket(rendezvousKey, { type: 'advert', body: advertBody })
926
933
  const event = await signNostrEvent(
927
934
  NOSTR_ADVERT_KIND,
928
- [['t', rendezvousKey], ['x', 'advert'], ['d', rendezvousKey]],
935
+ [NOSTR_TOPIC_TAG, ['t', rendezvousKey], ['x', 'advert'], ['d', rendezvousKey]],
929
936
  bytesToBase64(bytes),
930
937
  secretKey,
931
938
  )
@@ -957,7 +964,7 @@ export function createNostrDiscoveryProvider(options = {}) {
957
964
  const rendezvousKey = nodeRendezvousKey(hash)
958
965
  const event = await signNostrEvent(
959
966
  NOSTR_SIGNAL_KIND,
960
- [['t', rendezvousKey], ['x', 'signal'], ['p', hash]],
967
+ [NOSTR_TOPIC_TAG, ['t', rendezvousKey], ['x', 'signal'], ['p', hash]],
961
968
  bytesToBase64(bytes),
962
969
  secretKey,
963
970
  )
@@ -1019,7 +1026,7 @@ export function createNostrDiscoveryProvider(options = {}) {
1019
1026
  const bytes = encryptSignalPacket(rendezvousKey, { type: 'advert', body: advertBody })
1020
1027
  const event = await signNostrEvent(
1021
1028
  NOSTR_ADVERT_KIND,
1022
- [['t', rendezvousKey], ['x', 'advert'], ['d', rendezvousKey]],
1029
+ [NOSTR_TOPIC_TAG, ['t', rendezvousKey], ['x', 'advert'], ['d', rendezvousKey]],
1023
1030
  bytesToBase64(bytes),
1024
1031
  secretKey,
1025
1032
  )
package/link/frame.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
- import { bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
3
+ import { bytesToBase64, bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
4
4
 
5
5
  /** frameId 字段字节长度(128 位)。 */
6
6
  export const FRAME_ID_BYTES = 16
@@ -40,6 +40,28 @@ export function randomFrameIdHex() {
40
40
  return bytesToHex(randomBytes(FRAME_ID_BYTES))
41
41
  }
42
42
 
43
+ /**
44
+ * 在单包字符上限下,求单片最大可承载 chunk 字节数。
45
+ * 以 base64 逐片精确贴合上限为目标,用二分在 [0, limit] 内找最大 chunkBytes,
46
+ * 使 base64Len(headerBytes + chunkBytes) <= limit —— 最大化单帧载荷利用率,
47
+ * 而不是按 base64 膨胀预留固定比例(预留比例会浪费余量且对帧头/填充不精确)。
48
+ * @param {number} maxPayloadChars 单包载荷字符上限(编码后)
49
+ * @param {(bytes: Uint8Array) => string} [encode] 载荷编码函数(默认 base64)
50
+ * @param {number} [headerBytes=FRAME_HEADER_BYTES] 帧头字节数
51
+ * @returns {number} 最大 chunk 字节数(>=0)
52
+ */
53
+ export function maxFrameChunkBytesForPayload(maxPayloadChars, encode = bytesToBase64, headerBytes = FRAME_HEADER_BYTES) {
54
+ const limit = Math.max(1, Math.floor(Number(maxPayloadChars) || 0))
55
+ let lowerBound = 0
56
+ let upperBound = limit
57
+ while (lowerBound < upperBound) {
58
+ const candidateChunkBytes = Math.ceil((lowerBound + upperBound + 1) / 2)
59
+ if (encode(new Uint8Array(headerBytes + candidateChunkBytes)).length <= limit) lowerBound = candidateChunkBytes
60
+ else upperBound = candidateChunkBytes - 1
61
+ }
62
+ return lowerBound
63
+ }
64
+
43
65
  /**
44
66
  * 将消息切成带帧头的分片。
45
67
  * @param {string | Uint8Array} frameId 消息 id(hex 或 16 字节)
@@ -50,7 +72,7 @@ export function randomFrameIdHex() {
50
72
  export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_CHUNK_BYTES) {
51
73
  const body = toBytes(bytes)
52
74
  const idBytes = normalizeFrameIdBytes(frameId)
53
- const chunkBytes = Math.max(256, Math.min(DEFAULT_MAX_MESSAGE_BYTES, Number(maxChunkBytes) || DEFAULT_MAX_FRAME_CHUNK_BYTES))
75
+ const chunkBytes = Math.min(DEFAULT_MAX_MESSAGE_BYTES, Number(maxChunkBytes) || DEFAULT_MAX_FRAME_CHUNK_BYTES)
54
76
  const total = Math.max(1, Math.ceil(body.byteLength / chunkBytes))
55
77
  /** @type {Uint8Array[]} */
56
78
  const frames = []
@@ -22,8 +22,7 @@ export const LINK_HANDSHAKE_DOMAIN = 'fount-link'
22
22
  export function normalizeLinkBinding(value) {
23
23
  const dtls = normalizeDtlsFingerprint(value)
24
24
  if (dtls) return dtls
25
- const hex = normalizeHex64(value)
26
- return isHex64(hex) ? hex : null
25
+ return isHex64(value)
27
26
  }
28
27
 
29
28
  /**
package/link/pipe.mjs CHANGED
@@ -4,7 +4,7 @@ import { ms } from '../utils/duration.mjs'
4
4
  import { emitSafe } from '../utils/emit_safe.mjs'
5
5
  import { createLruMap } from '../utils/lru.mjs'
6
6
 
7
- import { createReassembler, encodeFrames, randomFrameIdHex } from './frame.mjs'
7
+ import { createReassembler, DEFAULT_MAX_FRAME_CHUNK_BYTES, encodeFrames, FRAME_HEADER_BYTES, randomFrameIdHex } from './frame.mjs'
8
8
  import { buildAuth, buildHello, parseHello, verifyAuth } from './handshake.mjs'
9
9
 
10
10
  const encoder = new TextEncoder()
@@ -81,10 +81,14 @@ export function asLinkHandle(pipe, extras = {}) {
81
81
  * @param {number} [options.idleTimeoutMs] 空闲超时
82
82
  * @param {number} [options.handshakeTimeoutMs] 握手超时
83
83
  * @param {number} [options.rttWindowSize] RTT 样本滑动窗口大小
84
+ * @param {number} [options.maxFrameBytes] 单帧最大字节数(provider 按其传输载荷上限折算;默认 15 KiB)
84
85
  * @returns {object} link 句柄 + 入站 API
85
86
  */
86
87
  export function createLinkPipe(options) {
87
88
  const { providerId, level } = options
89
+ const maxFrameBytes = options.maxFrameBytes == null ? DEFAULT_MAX_FRAME_CHUNK_BYTES : Number(options.maxFrameBytes)
90
+ if (maxFrameBytes < FRAME_HEADER_BYTES)
91
+ throw new Error(`p2p: ${providerId} maxFrameBytes too small to carry a frame header`)
88
92
  const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
89
93
  const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
90
94
  const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
@@ -348,7 +352,7 @@ export function createLinkPipe(options) {
348
352
  payload: envelope.payload ?? null,
349
353
  frameId,
350
354
  }))
351
- for (const frame of encodeFrames(frameId, bytes)) {
355
+ for (const frame of encodeFrames(frameId, bytes, maxFrameBytes)) {
352
356
  const sent = options.sendFrame(envelope.action, frame)
353
357
  if (sent?.then) await sent
354
358
  sentFrames++
@@ -4,16 +4,102 @@ import { randomBytes } from 'node:crypto'
4
4
  import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
5
5
  import { normalizeHex64 } from '../../core/hexIds.mjs'
6
6
  import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
7
- import { resolveNostrRelayUrls } from '../../discovery/nostr.mjs'
7
+ import { NOSTR_SIGNAL_KIND, resolveNostrRelayUrls } from '../../discovery/nostr.mjs'
8
8
  import { ms } from '../../utils/duration.mjs'
9
9
  import { createLruMap } from '../../utils/lru.mjs'
10
+ import { FRAME_HEADER_BYTES, maxFrameChunkBytesForPayload } from '../frame.mjs'
10
11
  import { asLinkHandle } from '../pipe.mjs'
11
12
 
12
13
  import { LINK_LEVEL_NOSTR } from './levels.mjs'
13
14
  import { createLinkIdBoundPipe } from './link_id_pipe.mjs'
14
15
 
15
- /** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。 */
16
- const MAX_LINK_PAYLOAD_CHARS = 12 * 1024
16
+ /** 单包 payload(UTF-8 / base64)上限,避免撞 relay content 限制。
17
+ * 默认兜底取 2026-08 本机对默认公共 relay 的 NIP-11 `max_message_length` 非零最小值(131072 = nostr.mom)。
18
+ * 有 relay 信息时用实测非零最小值覆盖(见 refreshPayloadCap),无 relay / 未探测到时用此默认。 */
19
+ export const MAX_LINK_PAYLOAD_CHARS = 131072
20
+
21
+ /** relay info(NIP-11)单次探测超时。 */
22
+ const RELAY_INFO_TIMEOUT_MS = ms('4s')
23
+ /** 实测 payload 上限缓存有效期。 */
24
+ const PAYLOAD_CAP_CACHE_TTL_MS = ms('10m')
25
+ const textEncoder = new TextEncoder()
26
+ /** 固定 64 位 hex 占位(id/pubkey/sig/rendezvousKey/nodeHash 均固定宽度)。 */
27
+ const HEX64 = 'a'.repeat(64)
28
+ /** AES-GCM 封装输出中的固定长度字段占位:iv = base64(12B) = 16,authTag = base64(16B) = 24。 */
29
+ const GCM_IV_BASE64 = 'a'.repeat(16)
30
+ const GCM_AUTH_TAG_BASE64 = 'a'.repeat(24)
31
+
32
+ /**
33
+ * 估算把给定 link 包发布为 Nostr EVENT 后,完整 WebSocket 消息(["EVENT", event])的 UTF-8 字节长度。
34
+ * id/pubkey/sig/rendezvousKey/nodeHash 均为固定 64 hex,created_at/kind/tags 固定,故长度仅随 packet.payload 变化,
35
+ * 可同步精确构造(含加密封装壳与 event 字段),无需真正 AES-GCM 封装与 Schnorr 签名。
36
+ * @param {object} packet link 包
37
+ * @returns {number} 完整消息字节长度
38
+ */
39
+ export function estimateEventMessageBytes(packet) {
40
+ // encryptSignalPacket 输出全 ASCII:{"iv":<16>,"authTag":<24>,"ciphertext":base64(packetJson bytes)}。
41
+ return textEncoder.encode(JSON.stringify(['EVENT', {
42
+ id: HEX64,
43
+ pubkey: HEX64,
44
+ created_at: Math.floor(Date.now() / 1000),
45
+ kind: NOSTR_SIGNAL_KIND,
46
+ tags: [['t', HEX64], ['x', 'signal'], ['p', HEX64]],
47
+ content: bytesToBase64(textEncoder.encode(JSON.stringify({
48
+ iv: GCM_IV_BASE64,
49
+ authTag: GCM_AUTH_TAG_BASE64,
50
+ ciphertext: bytesToBase64(textEncoder.encode(JSON.stringify(packet))),
51
+ }))),
52
+ sig: HEX64,
53
+ }])).length
54
+ }
55
+
56
+ /** relay cap 低于此字符数视为无法承载最小正 chunk(帧头 + 1 字节 chunk 的完整 EVENT 封装),从统一上限中剔除。
57
+ * 这类 relay 即便能传也无法携带有效载荷(maxFrameChunkBytesForPayload 得 0),参与取最小值只会无谓拖低/毒化整条链路。 */
58
+ export const MIN_USABLE_RELAY_CAP_CHARS = estimateEventMessageBytes({
59
+ type: 'link',
60
+ op: 'b',
61
+ from: HEX64,
62
+ linkId: HEX64,
63
+ payload: bytesToBase64(new Uint8Array(FRAME_HEADER_BYTES + 1)),
64
+ })
65
+
66
+ /**
67
+ * 拉取单个 relay 的 NIP-11 relay info 并读取 max_message_length。
68
+ * @param {string} relayUrl relay URL
69
+ * @returns {Promise<number | null>} 非零上限;失败/未声明返回 null
70
+ */
71
+ async function queryRelayMaxMessageLength(relayUrl) {
72
+ const httpUrl = relayUrl.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:')
73
+ const controller = new AbortController()
74
+ const timer = setTimeout(() => controller.abort(), RELAY_INFO_TIMEOUT_MS)
75
+ timer.unref?.()
76
+ try {
77
+ const response = await fetch(httpUrl, { headers: { Accept: 'application/nostr+json' }, signal: controller.signal })
78
+ const info = await response.json()
79
+ const limit = { ...info?.limit || {}, ...info?.limitation || {} }
80
+ const value = Number(limit.max_message_length)
81
+ return Number.isFinite(value) && value > 0 ? value : null
82
+ }
83
+ catch {
84
+ return null
85
+ }
86
+ finally {
87
+ clearTimeout(timer)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 从各 relay 上报的 cap 中取「可用」(>= 最小可用帧,能产生正数 chunk budget)的非零最小值。
93
+ * cap 过低的 relay 无法承载最小正 chunk(完整 EVENT 封装装不下),取最小值只会拖低/毒化整条链路,故剔除;
94
+ * 剩余 relay 的最小值保证任意一个可用 relay 都能传该帧(publishEvent 只要求任一 relay 接受)。
95
+ * @param {Array<number | null | undefined>} caps 各 relay 上报的 cap
96
+ * @returns {number | null} 可用最小值;无可用 relay 返回 null
97
+ */
98
+ export function minUsablePayloadCap(caps) {
99
+ const usable = caps.filter(value => Number.isFinite(value) && value >= MIN_USABLE_RELAY_CAP_CHARS)
100
+ return usable.length ? Math.min(...usable) : null
101
+ }
102
+
17
103
  /** open 到达前为同一 linkId 暂存的 c/b 包上限。 */
18
104
  const PENDING_PACKETS_MAX = 32
19
105
  /** Nostr 链握手超时(relay RTT 更慢)。 */
@@ -35,11 +121,49 @@ async function publishLinkPacket(remoteNodeHash, packet) {
35
121
  /**
36
122
  * 创建 Nostr 末位数据链路 provider(level = -∞)。
37
123
  * @param {{ getRelayUrls?: () => string[] }} [options] 中继解析(测试可注入)
38
- * @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
124
+ * @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void | Promise<void> }} provider
39
125
  */
40
126
  export function createNostrLinkProvider(options = {}) {
41
127
  const resolveRelayUrls = options.getRelayUrls || resolveNostrRelayUrls
42
128
 
129
+ /** @type {number | null} 本实例实测非零最小 payload 上限(NIP-11 max_message_length) */
130
+ let queriedPayloadCap = null
131
+ /** @type {number} 本实例最近一次实测时间戳 */
132
+ let payloadCapQueriedAt = 0
133
+ /** @type {Promise<void> | null} 本实例进行中的探测(去重并发) */
134
+ let payloadCapRefreshPromise = null
135
+
136
+ /**
137
+ * 探测各 relay 的 payload 上限,缓存「可用 relay」的最小值(TTL 内不重复探测)。
138
+ * 仅在实际 wss/ws/https/http 中继上探测;失败或未声明的 relay 忽略。
139
+ * @param {string[]} relayUrls 中继 URL 列表
140
+ * @returns {Promise<void>}
141
+ */
142
+ async function refreshPayloadCap(relayUrls) {
143
+ if (payloadCapRefreshPromise) return payloadCapRefreshPromise
144
+ if (queriedPayloadCap != null && Date.now() - payloadCapQueriedAt < PAYLOAD_CAP_CACHE_TTL_MS) return
145
+ const urls = [...new Set(relayUrls.filter(url => /^(wss?|https?):\/\//i.test(url)))]
146
+ if (!urls.length) return
147
+ payloadCapRefreshPromise = Promise.allSettled(urls.map(queryRelayMaxMessageLength))
148
+ .then(results => {
149
+ const value = minUsablePayloadCap(results.map(result => result.status === 'fulfilled' ? result.value : null))
150
+ if (value != null) {
151
+ queriedPayloadCap = value
152
+ payloadCapQueriedAt = Date.now()
153
+ }
154
+ })
155
+ .finally(() => { payloadCapRefreshPromise = null })
156
+ return payloadCapRefreshPromise
157
+ }
158
+
159
+ /**
160
+ * 当前生效的单包上限:有 relay 实测非零最小值则用它(最大化载荷利用率),否则用默认兜底。
161
+ * @returns {number} 上限(针对完整 Nostr EVENT 消息的字节数)
162
+ */
163
+ function currentMaxPayloadChars() {
164
+ return queriedPayloadCap ?? MAX_LINK_PAYLOAD_CHARS
165
+ }
166
+
43
167
  /** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
44
168
  let onInbound = null
45
169
  /** @type {object | null} */
@@ -109,11 +233,10 @@ export function createNostrLinkProvider(options = {}) {
109
233
  * @param {string} linkId 链路 id
110
234
  * @param {string} op open|c|b|close
111
235
  * @param {string} [payload] 可选载荷
236
+ * @param {number} [maxChars=MAX_LINK_PAYLOAD_CHARS] 该 pipe 冻结的字符上限
112
237
  * @returns {Promise<void>}
113
238
  */
114
- async function sendOp(remoteNodeHash, linkId, op, payload) {
115
- if (payload != null && payload.length > MAX_LINK_PAYLOAD_CHARS)
116
- throw new Error('p2p: nostr link payload too large')
239
+ async function sendOp(remoteNodeHash, linkId, op, payload, maxChars = MAX_LINK_PAYLOAD_CHARS) {
117
240
  const packet = {
118
241
  type: 'link',
119
242
  op,
@@ -121,6 +244,8 @@ export function createNostrLinkProvider(options = {}) {
121
244
  linkId,
122
245
  }
123
246
  if (payload != null) packet.payload = payload
247
+ if (estimateEventMessageBytes(packet) > maxChars)
248
+ throw new Error('p2p: nostr link payload too large')
124
249
  await publishLinkPacket(remoteNodeHash, packet)
125
250
  }
126
251
 
@@ -130,6 +255,19 @@ export function createNostrLinkProvider(options = {}) {
130
255
  */
131
256
  function openPipe(opts) {
132
257
  const { linkId, remoteNodeHash, initiator } = opts
258
+ // 冻结该 pipe 的字符上限:按完整 Nostr EVENT 消息字节数切帧,与发送校验共用同一上限,保持自洽。
259
+ const payloadChars = currentMaxPayloadChars()
260
+ const maxFrameBytes = maxFrameChunkBytesForPayload(
261
+ payloadChars,
262
+ // maxFrameChunkBytesForPayload 用 encode(...).length 度量载荷,故返回等长字符串。
263
+ frameBytes => 'x'.repeat(estimateEventMessageBytes({
264
+ type: 'link',
265
+ op: 'b',
266
+ from: (opts.localIdentity || localIdentity)?.nodeHash || '',
267
+ linkId,
268
+ payload: bytesToBase64(frameBytes),
269
+ })),
270
+ )
133
271
  const pipe = createLinkIdBoundPipe({
134
272
  providerId: 'nostr',
135
273
  level: LINK_LEVEL_NOSTR,
@@ -137,6 +275,7 @@ export function createNostrLinkProvider(options = {}) {
137
275
  linkId,
138
276
  nodeHash: remoteNodeHash,
139
277
  localIdentity: opts.localIdentity || localIdentity,
278
+ maxFrameBytes,
140
279
  handshakeTimeoutMs: NOSTR_HANDSHAKE_TIMEOUT_MS,
141
280
  heartbeatMs: NOSTR_HEARTBEAT_MS,
142
281
  idleTimeoutMs: NOSTR_IDLE_TIMEOUT_MS,
@@ -145,7 +284,7 @@ export function createNostrLinkProvider(options = {}) {
145
284
  * @returns {Promise<void>}
146
285
  */
147
286
  async sendControlText(text) {
148
- await sendOp(remoteNodeHash, linkId, 'c', text)
287
+ await sendOp(remoteNodeHash, linkId, 'c', text, payloadChars)
149
288
  },
150
289
  /**
151
290
  * @param {string} _action action
@@ -153,7 +292,7 @@ export function createNostrLinkProvider(options = {}) {
153
292
  * @returns {Promise<void>}
154
293
  */
155
294
  async sendFrame(_action, frame) {
156
- await sendOp(remoteNodeHash, linkId, 'b', bytesToBase64(frame))
295
+ await sendOp(remoteNodeHash, linkId, 'b', bytesToBase64(frame), payloadChars)
157
296
  },
158
297
  /**
159
298
  * @returns {Promise<void>}
@@ -174,9 +313,9 @@ export function createNostrLinkProvider(options = {}) {
174
313
  /**
175
314
  * 入站已解密的 link 包(由信令 demux 调用)。
176
315
  * @param {object} packet link 包
177
- * @returns {void}
316
+ * @returns {Promise<void>}
178
317
  */
179
- function deliverPacket(packet) {
318
+ async function deliverPacket(packet) {
180
319
  if (packet?.type !== 'link') return
181
320
  const linkId = normalizeHex64(packet.linkId)
182
321
  const from = normalizeHex64(packet.from)
@@ -187,6 +326,8 @@ export function createNostrLinkProvider(options = {}) {
187
326
  if (op === 'open') {
188
327
  if (sessions.has(linkId)) return
189
328
  if (!onInbound || !localIdentity) return
329
+ await refreshPayloadCap(resolveRelayUrls())
330
+ if (sessions.has(linkId)) return
190
331
  const pipe = openPipe({
191
332
  linkId,
192
333
  remoteNodeHash: from,
@@ -238,6 +379,7 @@ export function createNostrLinkProvider(options = {}) {
238
379
  if (!remoteNodeHash) return null
239
380
  localIdentity = dialOptions.localIdentity || localIdentity
240
381
  if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
382
+ await refreshPayloadCap(resolveRelayUrls())
241
383
  const linkId = randomBytes(32).toString('hex')
242
384
  const pipe = openPipe({
243
385
  linkId,
@@ -256,6 +398,7 @@ export function createNostrLinkProvider(options = {}) {
256
398
  ensureListening(handlers) {
257
399
  onInbound = handlers.onInbound
258
400
  localIdentity = handlers.localIdentity
401
+ void refreshPayloadCap(resolveRelayUrls())
259
402
  return () => {
260
403
  onInbound = null
261
404
  for (const session of sessions.values())
package/node/network.mjs CHANGED
@@ -53,9 +53,7 @@ export function normalizeNetwork(raw) {
53
53
  * @returns {string[]} 去重 nodeHash 列表
54
54
  */
55
55
  const pickIds = key => [...new Set(
56
- (file[key] || [])
57
- .map(id => normalizeHex64(id))
58
- .filter(id => isHex64(id)),
56
+ (file[key] || []).map(isHex64).filter(Boolean),
59
57
  )]
60
58
  const hints = (file.hints || [])
61
59
  .map(hint => ({
@@ -221,9 +219,9 @@ export function promoteExplorePeer(nodeHash) {
221
219
  export function replaceNetworkPeerPools(pools = {}) {
222
220
  const net = loadNetwork()
223
221
  if (Array.isArray(pools.trustedPeers))
224
- net.trustedPeers = pools.trustedPeers.map(id => normalizeHex64(id)).filter(id => isHex64(id))
222
+ net.trustedPeers = pools.trustedPeers.map(isHex64).filter(Boolean)
225
223
  if (Array.isArray(pools.explorePeers))
226
- net.explorePeers = pools.explorePeers.map(id => normalizeHex64(id)).filter(id => isHex64(id))
224
+ net.explorePeers = pools.explorePeers.map(isHex64).filter(Boolean)
227
225
  net.lastRosterAt = Date.now()
228
226
  saveNetwork(net)
229
227
  }
@@ -73,9 +73,7 @@ function loadSyncConfig() {
73
73
  * @returns {string[]} 规范化去重后的 64-hex 列表
74
74
  */
75
75
  function normalizeHashList(list) {
76
- return [...new Set((Array.isArray(list) ? list : [])
77
- .map(id => normalizeHex64(id))
78
- .filter(id => isHex64(id)))]
76
+ return [...new Set((Array.isArray(list) ? list : []).map(isHex64).filter(Boolean))]
79
77
  }
80
78
 
81
79
  /** 将当前 sync 配置写盘 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -19,7 +19,7 @@ function sanitizeDiscoveryAdvertisement(ad) {
19
19
  title: String(ad.title || '').slice(0, 200),
20
20
  blurb: String(ad.blurb || '').slice(0, 500),
21
21
  advertiserPubKeyHash,
22
- advertiserNodeHash: isHex64(advertiserNodeHash) ? advertiserNodeHash : String(ad.advertiserNodeHash || ''),
22
+ advertiserNodeHash: isHex64(advertiserNodeHash) || String(ad.advertiserNodeHash || ''),
23
23
  observedAt: Number(ad.observedAt) || 0,
24
24
  signature,
25
25
  }