@steve02081504/fount-p2p 0.0.12 → 0.0.13
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 +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
|
|
2
|
+
import { isNodeInitialized } from '../node/instance.mjs'
|
|
3
|
+
import { setOverlayRateGate, clearOverlayRateGate } from '../overlay/index.mjs'
|
|
4
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
|
+
import { attachNodeScopeMailbox } from '../transport/node_scope.mjs'
|
|
6
|
+
|
|
7
|
+
import { attachInfraDebugLog, detachInfraDebugLog } from './debug_log.mjs'
|
|
8
|
+
import {
|
|
9
|
+
applyPriorityToRegistry,
|
|
10
|
+
clearInfraPriorityFromRegistry,
|
|
11
|
+
getInfraPriority,
|
|
12
|
+
setInfraPriority,
|
|
13
|
+
} from './priority.mjs'
|
|
14
|
+
import infraTunables from './tunables.json' with { type: 'json' }
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, { tokens: number, updatedAt: number }>} */
|
|
17
|
+
const overlayRateBuckets = new Map()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Token bucket:桶容量 = burst,补充速率 = perMin/min。
|
|
21
|
+
* @param {Map<string, { tokens: number, updatedAt: number }>} buckets - 每 sender 桶状态
|
|
22
|
+
* @param {string} sender - 发送方 nodeHash
|
|
23
|
+
* @param {number} now - 当前时间戳(ms)
|
|
24
|
+
* @param {{ perMin: number, burst: number }} limits - 限速参数
|
|
25
|
+
* @returns {boolean} 是否允许本次 overlay 动作
|
|
26
|
+
*/
|
|
27
|
+
export function consumeOverlayRateToken(buckets, sender, now, limits) {
|
|
28
|
+
const perMin = Math.max(1, limits.perMin)
|
|
29
|
+
const burst = Math.max(1, limits.burst)
|
|
30
|
+
const refillPerMs = perMin / 60_000
|
|
31
|
+
let bucket = buckets.get(sender)
|
|
32
|
+
if (!bucket) bucket = { tokens: burst, updatedAt: now }
|
|
33
|
+
const elapsed = Math.max(0, now - bucket.updatedAt)
|
|
34
|
+
bucket.tokens = Math.min(burst, bucket.tokens + elapsed * refillPerMs)
|
|
35
|
+
bucket.updatedAt = now
|
|
36
|
+
if (bucket.tokens < 1) {
|
|
37
|
+
buckets.set(sender, bucket)
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
bucket.tokens -= 1
|
|
41
|
+
buckets.set(sender, bucket)
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
function installOverlayRateLimit() {
|
|
49
|
+
const limits = {
|
|
50
|
+
perMin: Math.max(1, Number(infraTunables.overlayRatePerMin) || 120),
|
|
51
|
+
burst: Math.max(1, Number(infraTunables.overlayRateBurst) || 30),
|
|
52
|
+
}
|
|
53
|
+
setOverlayRateGate((sender, action) => {
|
|
54
|
+
if (action !== 'route_req' && action !== 'relay') return true
|
|
55
|
+
return consumeOverlayRateToken(overlayRateBuckets, sender, Date.now(), limits)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function removeOverlayRateLimit() {
|
|
63
|
+
clearOverlayRateGate()
|
|
64
|
+
overlayRateBuckets.clear()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {boolean} */
|
|
68
|
+
let infraRunning = false
|
|
69
|
+
|
|
70
|
+
/** @type {(() => void) | null} */
|
|
71
|
+
let mailboxDispose = null
|
|
72
|
+
|
|
73
|
+
/** @type {(() => void) | null} */
|
|
74
|
+
let debugDispose = null
|
|
75
|
+
|
|
76
|
+
/** @type {number | null} */
|
|
77
|
+
let savedMaxActive = null
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {boolean} infra relay 是否在运行
|
|
81
|
+
*/
|
|
82
|
+
export function isInfraRunning() {
|
|
83
|
+
return infraRunning
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 启动 public-good infra:overlay、mailbox、rate limit、priority、debug log。
|
|
88
|
+
* @param {{ maxActive?: number, logger?: { info?: Function, warn?: Function, error?: Function, log?: Function } | null }} [options] - maxActive 与 debug logger
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
export async function startInfra(options = {}) {
|
|
92
|
+
if (!isNodeInitialized()) throw new Error('p2p: startInfra requires initNode')
|
|
93
|
+
if (infraRunning) {
|
|
94
|
+
if (options.maxActive != null) await getLinkRegistry().setMaxActive(options.maxActive)
|
|
95
|
+
if (options.logger !== undefined) {
|
|
96
|
+
detachInfraDebugLog()
|
|
97
|
+
debugDispose = attachInfraDebugLog(options.logger ?? null)
|
|
98
|
+
}
|
|
99
|
+
applyPriorityToRegistry()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const registry = getLinkRegistry()
|
|
103
|
+
await registry.ensureRuntime()
|
|
104
|
+
registry.ensureOverlayRouter()
|
|
105
|
+
installOverlayRateLimit()
|
|
106
|
+
savedMaxActive = registry.getMaxActive()
|
|
107
|
+
if (options.maxActive != null)
|
|
108
|
+
await registry.setMaxActive(options.maxActive)
|
|
109
|
+
else
|
|
110
|
+
await registry.setMaxActive(infraTunables.defaultMaxActive ?? savedMaxActive)
|
|
111
|
+
if (!mailboxDispose) mailboxDispose = attachNodeScopeMailbox()
|
|
112
|
+
const logger = options.logger === undefined ? console : options.logger ?? null
|
|
113
|
+
debugDispose = attachInfraDebugLog(logger)
|
|
114
|
+
applyPriorityToRegistry()
|
|
115
|
+
infraRunning = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 卸掉 infra 自己挂的面(mailbox / rate / debug / priority / maxActive 恢复)。
|
|
120
|
+
* 不碰用户另行 attach 的 wires,不绑信誉同步。
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
export async function stopInfra() {
|
|
124
|
+
if (!infraRunning) return
|
|
125
|
+
debugDispose?.()
|
|
126
|
+
debugDispose = null
|
|
127
|
+
detachInfraDebugLog()
|
|
128
|
+
clearInfraPriorityFromRegistry()
|
|
129
|
+
removeOverlayRateLimit()
|
|
130
|
+
mailboxDispose?.()
|
|
131
|
+
mailboxDispose = null
|
|
132
|
+
const registry = getLinkRegistry()
|
|
133
|
+
if (savedMaxActive != null)
|
|
134
|
+
await registry.setMaxActive(savedMaxActive)
|
|
135
|
+
savedMaxActive = null
|
|
136
|
+
infraRunning = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 再导出:infra 路由加权配置(见 `priority.mjs`)。 */
|
|
140
|
+
export { getInfraPriority, setInfraPriority }
|
package/link/frame.mjs
CHANGED
|
@@ -2,12 +2,10 @@ import { randomBytes } from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
import { bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
|
|
4
4
|
|
|
5
|
-
/** 二进制帧协议版本号。 */
|
|
6
|
-
export const FRAME_VERSION = 1
|
|
7
5
|
/** frameId 字段字节长度(128 位)。 */
|
|
8
6
|
export const FRAME_ID_BYTES = 16
|
|
9
|
-
/** 帧头:
|
|
10
|
-
export const FRAME_HEADER_BYTES =
|
|
7
|
+
/** 帧头:frameId(16) + seq(4) + total(4)。 */
|
|
8
|
+
export const FRAME_HEADER_BYTES = FRAME_ID_BYTES + 4 + 4
|
|
11
9
|
/** 默认单帧最大 chunk 大小(15 KiB)。 */
|
|
12
10
|
export const DEFAULT_MAX_FRAME_CHUNK_BYTES = 15 * 1024
|
|
13
11
|
/** 重组后消息最大字节数(8 MiB)。 */
|
|
@@ -62,11 +60,10 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
|
|
|
62
60
|
const end = Math.min(body.byteLength, start + chunkBytes)
|
|
63
61
|
const chunk = body.subarray(start, end)
|
|
64
62
|
const frame = new Uint8Array(FRAME_HEADER_BYTES + chunk.byteLength)
|
|
65
|
-
frame
|
|
66
|
-
frame.set(idBytes, 1)
|
|
63
|
+
frame.set(idBytes, 0)
|
|
67
64
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
|
|
68
|
-
view.setUint32(
|
|
69
|
-
view.setUint32(
|
|
65
|
+
view.setUint32(FRAME_ID_BYTES, seq, false)
|
|
66
|
+
view.setUint32(FRAME_ID_BYTES + 4, total, false)
|
|
70
67
|
frame.set(chunk, FRAME_HEADER_BYTES)
|
|
71
68
|
frames.push(frame)
|
|
72
69
|
}
|
|
@@ -76,23 +73,19 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
|
|
|
76
73
|
/**
|
|
77
74
|
* 解析单帧头与 chunk。
|
|
78
75
|
* @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧
|
|
79
|
-
* @returns {{
|
|
76
|
+
* @returns {{ frameId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
|
|
80
77
|
*/
|
|
81
78
|
export function decodeFrame(frame) {
|
|
82
79
|
const bytes = toBytes(frame)
|
|
83
80
|
if (bytes.byteLength < FRAME_HEADER_BYTES)
|
|
84
81
|
throw new Error('p2p: frame too short')
|
|
85
|
-
const
|
|
86
|
-
if (version !== FRAME_VERSION)
|
|
87
|
-
throw new Error(`p2p: unsupported frame version ${version}`)
|
|
88
|
-
const frameId = bytesToHex(bytes.subarray(1, 1 + FRAME_ID_BYTES))
|
|
82
|
+
const frameId = bytesToHex(bytes.subarray(0, FRAME_ID_BYTES))
|
|
89
83
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
90
|
-
const seq = view.getUint32(
|
|
91
|
-
const total = view.getUint32(
|
|
84
|
+
const seq = view.getUint32(FRAME_ID_BYTES, false)
|
|
85
|
+
const total = view.getUint32(FRAME_ID_BYTES + 4, false)
|
|
92
86
|
if (!total || seq >= total)
|
|
93
87
|
throw new Error('p2p: invalid frame sequence')
|
|
94
88
|
return {
|
|
95
|
-
version,
|
|
96
89
|
frameId,
|
|
97
90
|
seq,
|
|
98
91
|
total,
|
package/link/handshake.mjs
CHANGED
|
@@ -51,7 +51,7 @@ export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
|
|
|
51
51
|
/**
|
|
52
52
|
* 构造 link hello 握手包。
|
|
53
53
|
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
|
|
54
|
-
* @returns {{
|
|
54
|
+
* @returns {{ nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
|
|
55
55
|
*/
|
|
56
56
|
export function buildHello(options = {}) {
|
|
57
57
|
let publicKey = null
|
|
@@ -66,7 +66,7 @@ export function buildHello(options = {}) {
|
|
|
66
66
|
throw new Error('p2p: invalid hello fields')
|
|
67
67
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
68
68
|
throw new Error('p2p: hello nodePubKey does not match nodeHash')
|
|
69
|
-
return {
|
|
69
|
+
return { nodeHash, nodePubKey, nonce }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -92,13 +92,12 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
|
|
|
92
92
|
/**
|
|
93
93
|
* 解析并校验 hello 对象,无效时返回 null。
|
|
94
94
|
* @param {unknown} hello 原始 hello 载荷
|
|
95
|
-
* @returns {{
|
|
95
|
+
* @returns {{ nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
|
|
96
96
|
*/
|
|
97
97
|
export function parseHello(hello) {
|
|
98
98
|
const nodeHash = normalizeHex64(hello?.nodeHash)
|
|
99
99
|
const nodePubKey = normalizeHex64(hello?.nodePubKey)
|
|
100
100
|
const nonce = normalizeHex64(hello?.nonce)
|
|
101
|
-
if (Number(hello?.v) !== 1) return null
|
|
102
101
|
if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce)) return null
|
|
103
102
|
try {
|
|
104
103
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash) return null
|
|
@@ -106,7 +105,7 @@ export function parseHello(hello) {
|
|
|
106
105
|
catch {
|
|
107
106
|
return null
|
|
108
107
|
}
|
|
109
|
-
return {
|
|
108
|
+
return { nodeHash, nodePubKey, nonce }
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
/**
|
|
@@ -136,26 +135,26 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
|
136
135
|
|
|
137
136
|
/**
|
|
138
137
|
* 构造 discovery advert 待签名字节串。
|
|
139
|
-
* @param {string}
|
|
138
|
+
* @param {string} rendezvousKey discovery 内部汇合键
|
|
140
139
|
* @param {number} ts 时间戳(毫秒)
|
|
141
140
|
* @param {string} nodeHash 节点 nodeHash
|
|
142
141
|
* @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
|
|
143
142
|
* @returns {Uint8Array} 待签名消息字节
|
|
144
143
|
*/
|
|
145
|
-
export function buildAdvertMessage(
|
|
146
|
-
const base = `fount-advert\0${String(
|
|
144
|
+
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null) {
|
|
145
|
+
const base = `fount-advert\0${String(rendezvousKey)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
|
|
147
146
|
const port = normalizeTcpPort(tcpPort)
|
|
148
147
|
return Buffer.from(port ? `${base}\0${port}` : base, 'utf8')
|
|
149
148
|
}
|
|
150
149
|
|
|
151
150
|
/**
|
|
152
151
|
* 构造带签名的 discovery advert。
|
|
153
|
-
* @param {string}
|
|
152
|
+
* @param {string} rendezvousKey discovery 内部汇合键
|
|
154
153
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
155
154
|
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number } | null} [options] 签名身份与可选 tcpPort
|
|
156
155
|
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
|
|
157
156
|
*/
|
|
158
|
-
export async function buildSignedAdvert(
|
|
157
|
+
export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options = null) {
|
|
159
158
|
const seed = options?.secretKey
|
|
160
159
|
? Buffer.from(options.secretKey)
|
|
161
160
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
@@ -167,7 +166,7 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
|
|
|
167
166
|
const tcpPort = normalizeTcpPort(options?.tcpPort)
|
|
168
167
|
if (options?.tcpPort && !tcpPort)
|
|
169
168
|
throw new Error('p2p: advert tcpPort invalid')
|
|
170
|
-
const message = buildAdvertMessage(
|
|
169
|
+
const message = buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort)
|
|
171
170
|
const sig = await sign(message, secretKey)
|
|
172
171
|
const advert = {
|
|
173
172
|
nodeHash,
|
|
@@ -181,14 +180,14 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
|
|
|
181
180
|
|
|
182
181
|
/**
|
|
183
182
|
* 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash。
|
|
184
|
-
* @param {string}
|
|
183
|
+
* @param {string} rendezvousKey 期望的汇合键
|
|
185
184
|
* @param {unknown} advert 原始 advert 载荷
|
|
186
185
|
* @param {number} [now=Date.now()] 当前时间(毫秒)
|
|
187
186
|
* @param {number} [maxSkewMs=10 * 60_000] 允许的最大时钟偏差(毫秒)
|
|
188
187
|
* @returns {Promise<string | null>} 验证通过的 nodeHash,失败返回 null
|
|
189
188
|
*/
|
|
190
|
-
export async function verifySignedAdvert(
|
|
191
|
-
const parsedHello = parseHello({
|
|
189
|
+
export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
|
|
190
|
+
const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
|
|
192
191
|
if (!parsedHello) return null
|
|
193
192
|
const ts = Number(advert?.ts)
|
|
194
193
|
const sig = String(advert?.sig ?? '').trim().toLowerCase()
|
|
@@ -196,7 +195,7 @@ export async function verifySignedAdvert(topic, advert, now = Date.now(), maxSke
|
|
|
196
195
|
const hasTcpPortField = !!advert?.tcpPort
|
|
197
196
|
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
198
197
|
if (hasTcpPortField && !tcpPort) return null
|
|
199
|
-
const message = buildAdvertMessage(
|
|
198
|
+
const message = buildAdvertMessage(rendezvousKey, ts, parsedHello.nodeHash, tcpPort)
|
|
200
199
|
const ok = await verify(Buffer.from(sig, 'hex'), message, Buffer.from(parsedHello.nodePubKey, 'hex'))
|
|
201
200
|
return ok ? parsedHello.nodeHash : null
|
|
202
201
|
}
|
package/link/pipe.mjs
CHANGED
|
@@ -28,25 +28,25 @@ export function asLinkHandle(pipe, extras = {}) {
|
|
|
28
28
|
/** @returns {number} 提供者 level */
|
|
29
29
|
get level() { return pipe.level },
|
|
30
30
|
/**
|
|
31
|
-
* @param {...any}
|
|
31
|
+
* @param {...any} callArguments 透传 send
|
|
32
32
|
* @returns {ReturnType<typeof pipe.send>} 是否发送成功
|
|
33
33
|
*/
|
|
34
|
-
send: (...
|
|
34
|
+
send: (...callArguments) => pipe.send(...callArguments),
|
|
35
35
|
/**
|
|
36
|
-
* @param {...any}
|
|
36
|
+
* @param {...any} callArguments 透传 onEnvelope
|
|
37
37
|
* @returns {ReturnType<typeof pipe.onEnvelope>} 取消订阅
|
|
38
38
|
*/
|
|
39
|
-
onEnvelope: (...
|
|
39
|
+
onEnvelope: (...callArguments) => pipe.onEnvelope(...callArguments),
|
|
40
40
|
/**
|
|
41
|
-
* @param {...any}
|
|
41
|
+
* @param {...any} callArguments 透传 onDown
|
|
42
42
|
* @returns {ReturnType<typeof pipe.onDown>} 取消订阅
|
|
43
43
|
*/
|
|
44
|
-
onDown: (...
|
|
44
|
+
onDown: (...callArguments) => pipe.onDown(...callArguments),
|
|
45
45
|
/**
|
|
46
|
-
* @param {...any}
|
|
46
|
+
* @param {...any} callArguments 透传 close
|
|
47
47
|
* @returns {ReturnType<typeof pipe.close>} 关闭完成
|
|
48
48
|
*/
|
|
49
|
-
close: (...
|
|
49
|
+
close: (...callArguments) => pipe.close(...callArguments),
|
|
50
50
|
/** @returns {ReturnType<typeof pipe.stats>} 运行时统计 */
|
|
51
51
|
stats: () => pipe.stats(),
|
|
52
52
|
...extras,
|
|
@@ -207,9 +207,9 @@ export function createBleGattLinkProvider() {
|
|
|
207
207
|
* @returns {void}
|
|
208
208
|
*/
|
|
209
209
|
onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
|
|
210
|
-
const
|
|
211
|
-
if (sessionInbound) sessionInbound(
|
|
212
|
-
else void acceptPeripheralWrite(
|
|
210
|
+
const buffer = Buffer.from(data)
|
|
211
|
+
if (sessionInbound) sessionInbound(buffer)
|
|
212
|
+
else void acceptPeripheralWrite(buffer).catch(() => { })
|
|
213
213
|
callback(bleno.Characteristic.RESULT_SUCCESS)
|
|
214
214
|
},
|
|
215
215
|
})
|
|
@@ -228,12 +228,12 @@ export function createBleGattLinkProvider() {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
* @param {Buffer}
|
|
231
|
+
* @param {Buffer} buffer 入站写(首包应为 link-open)
|
|
232
232
|
* @returns {Promise<void>}
|
|
233
233
|
*/
|
|
234
|
-
async function acceptPeripheralWrite(
|
|
234
|
+
async function acceptPeripheralWrite(buffer) {
|
|
235
235
|
if (activeInbound || acceptInflight || !onInbound || !localIdentity) return
|
|
236
|
-
const opened = parseLinkOpen(
|
|
236
|
+
const opened = parseLinkOpen(buffer)
|
|
237
237
|
if (!opened) return
|
|
238
238
|
acceptInflight = true
|
|
239
239
|
try {
|
package/link/rtc.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import process from 'node:process'
|
|
|
2
2
|
|
|
3
3
|
import { toBytes } from '../core/bytes_codec.mjs'
|
|
4
4
|
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
|
|
@@ -20,9 +20,9 @@ process.on('exit', () => {
|
|
|
20
20
|
*/
|
|
21
21
|
export async function loadNodeRtcPolyfill() {
|
|
22
22
|
const mod = await import('node-datachannel/polyfill')
|
|
23
|
-
const {
|
|
23
|
+
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
24
24
|
return {
|
|
25
|
-
RTCPeerConnection:
|
|
25
|
+
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
|
|
26
26
|
RTCIceCandidate: mod.RTCIceCandidate,
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -54,8 +54,8 @@ export async function dispatchMailboxRecordsToConsumers(username, records) {
|
|
|
54
54
|
const ids = await handler(username, scoped)
|
|
55
55
|
for (const id of ids || []) delivered.add(String(id))
|
|
56
56
|
}
|
|
57
|
-
catch (
|
|
58
|
-
console.error('mailbox: consumer batch failed, retry per record',
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('mailbox: consumer batch failed, retry per record', error)
|
|
59
59
|
for (const row of scoped)
|
|
60
60
|
try {
|
|
61
61
|
const ids = await handler(username, [row])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
|
-
import {
|
|
3
|
+
import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
|
|
4
4
|
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
5
|
|
|
6
6
|
import { allowMailboxRelayForTier } from './importance.mjs'
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
* @returns {Promise<string | null>} 已验证的 remote nodeHash
|
|
24
24
|
*/
|
|
25
25
|
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
|
+
void username
|
|
26
27
|
if (!peerId) return null
|
|
27
|
-
const
|
|
28
|
-
const entry = slot?.getRoster()?.find(row => row.peerId === peerId)
|
|
28
|
+
const entry = activeLinkRoster().find(row => row.peerId === peerId)
|
|
29
29
|
const remote = entry?.remoteNodeHash?.trim().toLowerCase()
|
|
30
30
|
return remote || null
|
|
31
31
|
}
|
|
@@ -51,9 +51,9 @@ async function resolveRelayHopForIngress(record) {
|
|
|
51
51
|
* @returns {Promise<{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }>} 按在线 peer 数缩放的路由
|
|
52
52
|
*/
|
|
53
53
|
async function resolveRouting(username) {
|
|
54
|
+
void username
|
|
54
55
|
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
55
|
-
const
|
|
56
|
-
const peerCount = slot?.getRoster()?.length ?? 0
|
|
56
|
+
const peerCount = activeLinkRoster().length
|
|
57
57
|
return resolveMailboxRoutingForPeerCount(peerCount, mailbox, batterySaver)
|
|
58
58
|
}
|
|
59
59
|
|
package/mailbox/wire.mjs
CHANGED
|
@@ -11,30 +11,34 @@ import { parseMailboxGive, parseMailboxPut, parseMailboxWant } from './parse.mjs
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {MailboxWireContext} wireContext 入站上下文
|
|
14
|
-
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
|
-
* @returns {void}
|
|
14
|
+
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
16
16
|
*/
|
|
17
17
|
export function attachMailboxWire(wireContext, wire) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
const offs = [
|
|
19
|
+
wire.on('mailbox_put', (payload, peerId) => {
|
|
20
|
+
const put = parseMailboxPut(payload)
|
|
21
|
+
if (!put.ok) return
|
|
22
|
+
void ingestMailboxPut(wireContext, put.value, peerId).catch(error => console.error('mailbox: put ingest failed', error))
|
|
23
|
+
}),
|
|
24
|
+
wire.on('mailbox_want', (payload, peerId) => {
|
|
25
|
+
const want = parseMailboxWant(payload)
|
|
26
|
+
if (!want.ok) return
|
|
27
|
+
void respondMailboxWant(want.value, (giveWire, targetPeerId) => {
|
|
28
|
+
try {
|
|
29
|
+
wire.send('mailbox_give', giveWire, targetPeerId)
|
|
30
|
+
}
|
|
31
|
+
catch { /* disconnected */ }
|
|
32
|
+
}, peerId).catch(error => console.error('mailbox: want failed', error))
|
|
33
|
+
}),
|
|
34
|
+
wire.on('mailbox_give', payload => {
|
|
35
|
+
const give = parseMailboxGive(payload)
|
|
36
|
+
if (!give.ok) return
|
|
37
|
+
void ingestMailboxGive(wireContext, give.value).catch(error => console.error('mailbox: give ingest failed', error))
|
|
38
|
+
}),
|
|
39
|
+
]
|
|
40
|
+
return () => {
|
|
41
|
+
for (const off of offs)
|
|
42
|
+
try { off?.() } catch { /* ignore */ }
|
|
43
|
+
}
|
|
40
44
|
}
|
package/node/entity_store.mjs
CHANGED
|
@@ -80,9 +80,9 @@ export function createFsEntityStore(baseDir) {
|
|
|
80
80
|
const entries = await fsp.readdir(root, { withFileTypes: true })
|
|
81
81
|
return entries.filter(e => e.isDirectory()).map(e => e.name.toLowerCase()).filter(h => parseEntityHash(h))
|
|
82
82
|
}
|
|
83
|
-
catch (
|
|
84
|
-
if (/** @type {NodeJS.ErrnoException} */
|
|
85
|
-
throw
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return []
|
|
85
|
+
throw error
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
|
|
@@ -115,9 +115,9 @@ export function createFsEntityStore(baseDir) {
|
|
|
115
115
|
try {
|
|
116
116
|
return await fsp.readFile(filePath)
|
|
117
117
|
}
|
|
118
|
-
catch (
|
|
119
|
-
if (/** @type {NodeJS.ErrnoException} */
|
|
120
|
-
throw
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (/** @type {NodeJS.ErrnoException} */ error.code === 'ENOENT') return null
|
|
120
|
+
throw error
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
|