@steve02081504/fount-p2p 0.0.4 → 0.0.6
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/bytes_codec.mjs +0 -27
- package/core/constants.mjs +0 -31
- package/core/hexIds.mjs +0 -13
- package/crypto/key.mjs +14 -15
- package/dag/canonicalize_row.mjs +5 -5
- package/dag/index.mjs +0 -73
- package/dag/storage.mjs +0 -22
- package/discovery/mdns.mjs +4 -4
- package/discovery/nostr.mjs +3 -3
- package/federation/chunk_fetch_pending.mjs +3 -3
- package/federation/chunk_fetch_scheduler.mjs +3 -3
- package/federation/dag_order_cache.mjs +3 -3
- package/federation/entity_key_chain.mjs +0 -9
- package/federation/topo_order_memo.mjs +3 -18
- package/files/assemble.mjs +55 -39
- package/files/assemble_stream.mjs +96 -44
- package/files/chunk_provider_registry.mjs +0 -6
- package/files/chunk_responder.mjs +1 -1
- package/files/chunk_store.mjs +1 -11
- package/files/evfs.mjs +74 -47
- package/files/manifest.mjs +11 -2
- package/files/manifest_acl_registry.mjs +0 -6
- package/files/transfer_key.mjs +21 -15
- package/files/transfer_key_registry.mjs +9 -16
- package/governance/branch.mjs +0 -11
- package/governance/join_pow.mjs +17 -17
- package/link/channel_mux.mjs +9 -7
- package/link/frame.mjs +5 -5
- package/link/handshake.mjs +17 -17
- package/link/link.mjs +33 -33
- package/mailbox/deliver_or_store.mjs +13 -13
- package/mailbox/importance.mjs +0 -33
- package/mailbox/settings.mjs +0 -9
- package/mailbox/wire.mjs +4 -4
- package/node/denylist.mjs +0 -24
- package/node/network.mjs +0 -51
- package/node/personal_block.mjs +0 -35
- package/node/reputation_store.mjs +5 -14
- package/node/retention_policy.mjs +8 -23
- package/overlay/index.mjs +6 -6
- package/package.json +1 -1
- package/registries/event_type.mjs +0 -12
- package/registries/inbound.mjs +8 -30
- package/reputation/engine.mjs +0 -11
- package/rooms/scoped_link.mjs +18 -18
- package/schemas/part_query.mjs +156 -0
- package/timeline/append_core.mjs +3 -3
- package/transport/group_link_set.mjs +30 -30
- package/transport/ice_servers.mjs +0 -8
- package/transport/link_registry.mjs +13 -13
- package/transport/peer_identity_maps.mjs +0 -58
- package/transport/peer_pool.mjs +4 -4
- package/transport/remote_user_room.mjs +0 -12
- package/transport/rtc_connection_budget.mjs +2 -0
- package/transport/user_room.mjs +13 -24
- package/trust_graph/cache.mjs +0 -13
- package/trust_graph/engine.mjs +0 -24
- package/utils/async_mutex.mjs +0 -9
- package/wire/group_part.mjs +3 -3
- package/wire/part_fanout.mjs +0 -19
- package/wire/part_ingress.mjs +14 -14
- package/wire/part_query.mjs +486 -0
- package/wire/part_query.tunables.json +14 -0
- package/wire/part_query_cache.mjs +101 -0
package/governance/join_pow.mjs
CHANGED
|
@@ -85,40 +85,40 @@ export function powVoluntaryBonus(achievedBits, floorBits, tunables = admissionT
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* @param {object} powSolution 客户端 solution
|
|
88
|
-
* @param {object}
|
|
89
|
-
* @param {string}
|
|
90
|
-
* @param {string}
|
|
91
|
-
* @param {string[]}
|
|
92
|
-
* @param {number} [
|
|
93
|
-
* @param {number} [
|
|
94
|
-
* @param {number} [
|
|
95
|
-
* @param {number} [
|
|
88
|
+
* @param {object} options 校验上下文
|
|
89
|
+
* @param {string} options.groupId 群 ID
|
|
90
|
+
* @param {string} options.senderNodeHash 签名者 nodeHash(须与 joiner 绑定)
|
|
91
|
+
* @param {string[]} options.knownAnchors 近期 tip / checkpoint root 列表
|
|
92
|
+
* @param {number} [options.now=Date.now()] 当前时间
|
|
93
|
+
* @param {number} [options.difficultyBits=0] 准入 floor(前导零 bit)
|
|
94
|
+
* @param {number} [options.epochMs=JOIN_POW_DEFAULT_EPOCH_MS] epoch 长度
|
|
95
|
+
* @param {number} [options.epochSkew=JOIN_POW_DEFAULT_EPOCH_SKEW] 允许 epoch 偏移
|
|
96
96
|
* @returns {{ ok: boolean, achievedBits: number }} 校验结果与实际达成 bit
|
|
97
97
|
*/
|
|
98
|
-
export function verifyJoinPow(powSolution,
|
|
99
|
-
const floorBits = Math.max(0, Math.floor(Number(
|
|
98
|
+
export function verifyJoinPow(powSolution, options) {
|
|
99
|
+
const floorBits = Math.max(0, Math.floor(Number(options.difficultyBits) || 0))
|
|
100
100
|
if (floorBits <= 0) return { ok: true, achievedBits: 0 }
|
|
101
101
|
if (!powSolution || typeof powSolution !== 'object') return { ok: false, achievedBits: 0 }
|
|
102
102
|
|
|
103
103
|
const anchorRef = String(powSolution.anchorRef ?? '').trim()
|
|
104
104
|
const { nonce } = powSolution
|
|
105
105
|
const epoch = Number(powSolution.epoch)
|
|
106
|
-
const joinerNodeHash = String(powSolution.joinerNodeHash ??
|
|
107
|
-
const senderNodeHash = String(
|
|
106
|
+
const joinerNodeHash = String(powSolution.joinerNodeHash ?? options.senderNodeHash ?? '').trim().toLowerCase()
|
|
107
|
+
const senderNodeHash = String(options.senderNodeHash ?? '').trim().toLowerCase()
|
|
108
108
|
|
|
109
109
|
if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
|
|
110
110
|
if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
|
|
111
111
|
|
|
112
|
-
const anchors = (
|
|
112
|
+
const anchors = (options.knownAnchors ?? []).map(a => String(a).trim()).filter(Boolean)
|
|
113
113
|
if (!anchors.length || !anchors.includes(anchorRef)) return { ok: false, achievedBits: 0 }
|
|
114
114
|
|
|
115
|
-
const epochMs = Math.max(60_000, Number(
|
|
116
|
-
const skew = Math.max(0, Math.floor(Number(
|
|
117
|
-
const nowEpoch = Math.floor((
|
|
115
|
+
const epochMs = Math.max(60_000, Number(options.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
|
|
116
|
+
const skew = Math.max(0, Math.floor(Number(options.epochSkew) ?? JOIN_POW_DEFAULT_EPOCH_SKEW))
|
|
117
|
+
const nowEpoch = Math.floor((options.now ?? Date.now()) / epochMs)
|
|
118
118
|
if (Math.abs(epoch - nowEpoch) > skew) return { ok: false, achievedBits: 0 }
|
|
119
119
|
|
|
120
120
|
const hash = computeJoinPowHash({
|
|
121
|
-
groupId:
|
|
121
|
+
groupId: options.groupId,
|
|
122
122
|
anchorRef,
|
|
123
123
|
joinerNodeHash,
|
|
124
124
|
epoch,
|
package/link/channel_mux.mjs
CHANGED
|
@@ -38,6 +38,8 @@ const DEFAULT_ACTION_PRIORITIES = Object.freeze({
|
|
|
38
38
|
discovery_query_response: 3,
|
|
39
39
|
part_invoke: 3,
|
|
40
40
|
part_invoke_response: 3,
|
|
41
|
+
part_query_req: 5,
|
|
42
|
+
part_query_res: 5,
|
|
41
43
|
char_rpc: 3,
|
|
42
44
|
fed_chunk_put: 5,
|
|
43
45
|
fed_chunk_get: 5,
|
|
@@ -108,15 +110,15 @@ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CH
|
|
|
108
110
|
/**
|
|
109
111
|
* 订阅 bufferedamountlow 事件,返回取消订阅函数。
|
|
110
112
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
111
|
-
* @param {() => void}
|
|
113
|
+
* @param {() => void} callback 低水位回调
|
|
112
114
|
* @returns {() => void} 取消订阅函数
|
|
113
115
|
*/
|
|
114
|
-
export function onBufferedAmountLow(channel,
|
|
116
|
+
export function onBufferedAmountLow(channel, callback) {
|
|
115
117
|
/**
|
|
116
118
|
* bufferedamountlow 事件处理函数。
|
|
117
119
|
* @returns {void}
|
|
118
120
|
*/
|
|
119
|
-
const handler = () =>
|
|
121
|
+
const handler = () => callback()
|
|
120
122
|
channel.addEventListener?.('bufferedamountlow', handler)
|
|
121
123
|
channel.onbufferedamountlow = handler
|
|
122
124
|
if (channel.bufferedAmountLow?.subscribe)
|
|
@@ -129,12 +131,12 @@ export function onBufferedAmountLow(channel, cb) {
|
|
|
129
131
|
|
|
130
132
|
/**
|
|
131
133
|
* 创建带优先级队列的双通道发送器(control/bulk)。
|
|
132
|
-
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }}
|
|
134
|
+
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }} options 通道访问与高水位配置
|
|
133
135
|
* @returns {{ enqueue: (action: string, bytes: Uint8Array, preferredChannel?: 'control' | 'bulk') => void, flush: (channelName?: 'control' | 'bulk') => void, pending: () => { control: number, bulk: number }, clear: () => void }} 发送队列 API
|
|
134
136
|
*/
|
|
135
|
-
export function createChannelSendQueues(
|
|
136
|
-
const { getChannel } =
|
|
137
|
-
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(
|
|
137
|
+
export function createChannelSendQueues(options) {
|
|
138
|
+
const { getChannel } = options
|
|
139
|
+
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(options.highWatermarkBytes) || CHANNEL_HIGH_WATERMARK_BYTES)
|
|
138
140
|
/** @type {{ control: Array<{ priority: number, seq: number, bytes: Uint8Array }>, bulk: Array<{ priority: number, seq: number, bytes: Uint8Array }> }} */
|
|
139
141
|
const queues = { control: [], bulk: [] }
|
|
140
142
|
/** @type {{ control: boolean, bulk: boolean }} */
|
package/link/frame.mjs
CHANGED
|
@@ -150,13 +150,13 @@ function concatChunks(chunks) {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* 创建分片消息重组器。
|
|
153
|
-
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [
|
|
153
|
+
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [options] 大小、并发分片数与超时配置
|
|
154
154
|
* @returns {{ push: (frame: Uint8Array | ArrayBuffer | ArrayBufferView, now?: number) => Uint8Array | null, prune: (now?: number) => string[], clear: () => void, size: () => number }} 重组器 API
|
|
155
155
|
*/
|
|
156
|
-
export function createReassembler(
|
|
157
|
-
const maxMessageBytes = Math.max(1024, Number(
|
|
158
|
-
const maxPartials = Math.max(1, Number(
|
|
159
|
-
const partialTimeoutMs = Math.max(1000, Number(
|
|
156
|
+
export function createReassembler(options = {}) {
|
|
157
|
+
const maxMessageBytes = Math.max(1024, Number(options.maxMessageBytes) || DEFAULT_MAX_MESSAGE_BYTES)
|
|
158
|
+
const maxPartials = Math.max(1, Number(options.maxPartials) || DEFAULT_MAX_PARTIAL_MESSAGES)
|
|
159
|
+
const partialTimeoutMs = Math.max(1000, Number(options.partialTimeoutMs) || DEFAULT_PARTIAL_TIMEOUT_MS)
|
|
160
160
|
/** @type {Map<string, { total: number, chunks: Uint8Array[], seen: boolean[], bytes: number, firstSeenAt: number, lastSeenAt: number }>} */
|
|
161
161
|
const partials = new Map()
|
|
162
162
|
|
package/link/handshake.mjs
CHANGED
|
@@ -34,18 +34,18 @@ export function buildAuthMessage(peerNonce, localFingerprint, localNodeHash) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* 构造 link hello 握手包。
|
|
37
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [
|
|
37
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
|
|
38
38
|
* @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
|
|
39
39
|
*/
|
|
40
|
-
export function buildHello(
|
|
40
|
+
export function buildHello(options = {}) {
|
|
41
41
|
let publicKey = null
|
|
42
|
-
if (!
|
|
42
|
+
if (!options.nodeHash || !options.nodePubKey) {
|
|
43
43
|
const derived = keyPairFromSeed(Buffer.from(ensureNodeSeed(), 'hex'))
|
|
44
44
|
publicKey = derived.publicKey
|
|
45
45
|
}
|
|
46
|
-
const nodeHash = normalizeHex64(
|
|
47
|
-
const nodePubKey = normalizeHex64(
|
|
48
|
-
const nonce = normalizeHex64(
|
|
46
|
+
const nodeHash = normalizeHex64(options.nodeHash || getNodeHash())
|
|
47
|
+
const nodePubKey = normalizeHex64(options.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
48
|
+
const nonce = normalizeHex64(options.nonce || randomBytes(32).toString('hex'))
|
|
49
49
|
if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce))
|
|
50
50
|
throw new Error('p2p: invalid hello fields')
|
|
51
51
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
@@ -57,15 +57,15 @@ export function buildHello(opts = {}) {
|
|
|
57
57
|
* 对 link auth 消息签名。
|
|
58
58
|
* @param {string} peerNonce 对端 hello 中的 nonce
|
|
59
59
|
* @param {string} localFingerprint 本地 DTLS fingerprint
|
|
60
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [
|
|
60
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [options] 签名密钥与 nodeHash 覆盖
|
|
61
61
|
* @returns {Promise<{ sig: string }>} hex 签名
|
|
62
62
|
*/
|
|
63
|
-
export async function buildAuth(peerNonce, localFingerprint,
|
|
64
|
-
const seed =
|
|
65
|
-
? Buffer.from(
|
|
63
|
+
export async function buildAuth(peerNonce, localFingerprint, options = {}) {
|
|
64
|
+
const seed = options.secretKey
|
|
65
|
+
? Buffer.from(options.secretKey)
|
|
66
66
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
67
67
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
68
|
-
const nodeHash = normalizeHex64(
|
|
68
|
+
const nodeHash = normalizeHex64(options.nodeHash || pubKeyHash(publicKey))
|
|
69
69
|
if (nodeHash !== pubKeyHash(publicKey))
|
|
70
70
|
throw new Error('p2p: auth nodeHash does not match secretKey')
|
|
71
71
|
const message = buildAuthMessage(peerNonce, localFingerprint, nodeHash)
|
|
@@ -133,16 +133,16 @@ export function buildAdvertMessage(topic, ts, nodeHash) {
|
|
|
133
133
|
* 构造带签名的 discovery advert。
|
|
134
134
|
* @param {string} topic 广播主题
|
|
135
135
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
136
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string } | null} [
|
|
136
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string } | null} [options] 签名身份,省略则用本地节点
|
|
137
137
|
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string }>} 签名 advert
|
|
138
138
|
*/
|
|
139
|
-
export async function buildSignedAdvert(topic, ts = Date.now(),
|
|
140
|
-
const seed =
|
|
141
|
-
? Buffer.from(
|
|
139
|
+
export async function buildSignedAdvert(topic, ts = Date.now(), options = null) {
|
|
140
|
+
const seed = options?.secretKey
|
|
141
|
+
? Buffer.from(options.secretKey)
|
|
142
142
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
143
143
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
144
|
-
const nodeHash = normalizeHex64(
|
|
145
|
-
const nodePubKey = normalizeHex64(
|
|
144
|
+
const nodeHash = normalizeHex64(options?.nodeHash || pubKeyHash(publicKey))
|
|
145
|
+
const nodePubKey = normalizeHex64(options?.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
146
146
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
147
147
|
throw new Error('p2p: advert nodePubKey does not match nodeHash')
|
|
148
148
|
const message = buildAdvertMessage(topic, ts, nodeHash)
|
package/link/link.mjs
CHANGED
|
@@ -73,27 +73,27 @@ function formatErrorReason(error) {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* 建立 P2P link:WebRTC 双通道、握手、分帧收发与心跳。
|
|
76
|
-
* @param {object}
|
|
77
|
-
* @param {string | null} [
|
|
78
|
-
* @param {boolean}
|
|
79
|
-
* @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }}
|
|
80
|
-
* @param {RTCConfiguration['iceServers']} [
|
|
81
|
-
* @param {number} [
|
|
82
|
-
* @param {number} [
|
|
83
|
-
* @param {number} [
|
|
84
|
-
* @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [
|
|
85
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [
|
|
86
|
-
* @returns {Promise<{ ready: Promise<void>, get nodeHash(): string | null, send: (envelope: { scope: string, action: string, payload: unknown }) => Promise<boolean>, onEnvelope: (
|
|
76
|
+
* @param {object} options link 配置
|
|
77
|
+
* @param {string | null} [options.nodeHash] 期望的对端 nodeHash,省略则不校验
|
|
78
|
+
* @param {boolean} options.initiator 是否为连接发起方
|
|
79
|
+
* @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }} options.signal 信令收发接口
|
|
80
|
+
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
81
|
+
* @param {number} [options.heartbeatMs] 心跳间隔(毫秒)
|
|
82
|
+
* @param {number} [options.idleTimeoutMs] 无入站流量超时(毫秒)
|
|
83
|
+
* @param {number} [options.handshakeTimeoutMs] 握手超时(毫秒)
|
|
84
|
+
* @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [options.rtc] RTC 构造器,省略则加载 polyfill
|
|
85
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地握手身份
|
|
86
|
+
* @returns {Promise<{ ready: Promise<void>, get nodeHash(): string | null, send: (envelope: { scope: string, action: string, payload: unknown }) => Promise<boolean>, onEnvelope: (callback: (envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void) => () => void, onDown: (callback: (reason: string) => void) => () => void, close: (reason?: string) => Promise<void>, stats: () => object }>} link 句柄
|
|
87
87
|
*/
|
|
88
|
-
export async function createLink(
|
|
89
|
-
const heartbeatMs = Number(
|
|
90
|
-
const idleTimeoutMs = Number(
|
|
91
|
-
const handshakeTimeoutMs = Number(
|
|
88
|
+
export async function createLink(options) {
|
|
89
|
+
const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
|
|
90
|
+
const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
|
|
91
|
+
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
92
92
|
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
93
93
|
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
94
|
-
const rtc =
|
|
95
|
-
const pc = new rtc.RTCPeerConnection(
|
|
96
|
-
const targetNodeHash = normalizeHex64(
|
|
94
|
+
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
95
|
+
const pc = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
96
|
+
const targetNodeHash = normalizeHex64(options.nodeHash || '')
|
|
97
97
|
const remoteSignalQueue = []
|
|
98
98
|
const seenRemoteSignals = createLruMap(1024)
|
|
99
99
|
let remoteDescriptionSet = false
|
|
@@ -142,7 +142,7 @@ export async function createLink(opts) {
|
|
|
142
142
|
* @returns {Promise<void>}
|
|
143
143
|
*/
|
|
144
144
|
async function sendSignal(message) {
|
|
145
|
-
await Promise.resolve(
|
|
145
|
+
await Promise.resolve(options.signal.send(message))
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
@@ -185,7 +185,7 @@ export async function createLink(opts) {
|
|
|
185
185
|
const fingerprint = localFingerprint()
|
|
186
186
|
if (!fingerprint) return
|
|
187
187
|
authSent = true
|
|
188
|
-
await sendRawControl(await buildAuth(remoteHello.nonce, fingerprint,
|
|
188
|
+
await sendRawControl(await buildAuth(remoteHello.nonce, fingerprint, options.localIdentity ?? {}))
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
@@ -446,7 +446,7 @@ export async function createLink(opts) {
|
|
|
446
446
|
if (!helloSent) {
|
|
447
447
|
handshakeTimer = setTimeout(() => { void close('handshake-timeout') }, handshakeTimeoutMs)
|
|
448
448
|
helloSent = true
|
|
449
|
-
localHello = buildHello(
|
|
449
|
+
localHello = buildHello(options.localIdentity ?? {})
|
|
450
450
|
await sendRawControl(localHello)
|
|
451
451
|
}
|
|
452
452
|
await maybeSendAuth()
|
|
@@ -507,7 +507,7 @@ export async function createLink(opts) {
|
|
|
507
507
|
emitListeners(downListeners, reason)
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
unlistenRemote =
|
|
510
|
+
unlistenRemote = options.signal.onRemote(message => {
|
|
511
511
|
void handleRemoteSignal(message).catch(error => close(`signal-error:${formatErrorReason(error)}`))
|
|
512
512
|
}) ?? null
|
|
513
513
|
|
|
@@ -535,7 +535,7 @@ export async function createLink(opts) {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
if (
|
|
538
|
+
if (options.initiator) {
|
|
539
539
|
await attachChannel(pc.createDataChannel(CHANNEL_CONTROL))
|
|
540
540
|
await attachChannel(pc.createDataChannel(CHANNEL_BULK))
|
|
541
541
|
const offer = await pc.createOffer()
|
|
@@ -559,25 +559,25 @@ export async function createLink(opts) {
|
|
|
559
559
|
* 本端是否为连接发起方。
|
|
560
560
|
* @returns {boolean} 发起方标志
|
|
561
561
|
*/
|
|
562
|
-
get initiator() { return !!
|
|
562
|
+
get initiator() { return !!options.initiator },
|
|
563
563
|
send,
|
|
564
564
|
/**
|
|
565
565
|
* 订阅业务 envelope 入站。
|
|
566
|
-
* @param {(envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void}
|
|
566
|
+
* @param {(envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void} callback 回调
|
|
567
567
|
* @returns {() => void} 取消订阅函数
|
|
568
568
|
*/
|
|
569
|
-
onEnvelope(
|
|
570
|
-
envelopeListeners.add(
|
|
571
|
-
return () => envelopeListeners.delete(
|
|
569
|
+
onEnvelope(callback) {
|
|
570
|
+
envelopeListeners.add(callback)
|
|
571
|
+
return () => envelopeListeners.delete(callback)
|
|
572
572
|
},
|
|
573
573
|
/**
|
|
574
574
|
* 订阅 link 断开事件。
|
|
575
|
-
* @param {(reason: string) => void}
|
|
575
|
+
* @param {(reason: string) => void} callback 回调
|
|
576
576
|
* @returns {() => void} 取消订阅函数
|
|
577
577
|
*/
|
|
578
|
-
onDown(
|
|
579
|
-
downListeners.add(
|
|
580
|
-
return () => downListeners.delete(
|
|
578
|
+
onDown(callback) {
|
|
579
|
+
downListeners.add(callback)
|
|
580
|
+
return () => downListeners.delete(callback)
|
|
581
581
|
},
|
|
582
582
|
close,
|
|
583
583
|
/**
|
|
@@ -597,7 +597,7 @@ export async function createLink(opts) {
|
|
|
597
597
|
ready,
|
|
598
598
|
nodeHash: remoteNodeHash,
|
|
599
599
|
targetNodeHash: targetNodeHash || null,
|
|
600
|
-
initiator: !!
|
|
600
|
+
initiator: !!options.initiator,
|
|
601
601
|
connectionState: pc.connectionState,
|
|
602
602
|
iceConnectionState: pc.iceConnectionState,
|
|
603
603
|
lastInboundAt,
|
|
@@ -59,26 +59,26 @@ async function resolveRouting(username) {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* @param {string} username 副本用户名(trust graph 投递上下文)
|
|
62
|
-
* @param {object}
|
|
62
|
+
* @param {object} options 投递选项
|
|
63
63
|
* @returns {Promise<{ stored: boolean, delivered: boolean, relayed: number }>} 存转结果
|
|
64
64
|
*/
|
|
65
|
-
export async function deliverOrStoreMailboxPut(username,
|
|
65
|
+
export async function deliverOrStoreMailboxPut(username, options) {
|
|
66
66
|
const routing = await resolveRouting(username)
|
|
67
|
-
const toPubKeyHash = normalizeHex64(
|
|
67
|
+
const toPubKeyHash = normalizeHex64(options.toPubKeyHash)
|
|
68
68
|
if (!toPubKeyHash) return { stored: false, delivered: false, relayed: 0 }
|
|
69
|
-
const hop = normalizeMailboxHop(
|
|
69
|
+
const hop = normalizeMailboxHop(options.hop)
|
|
70
70
|
if (hop >= routing.maxHop) return { stored: false, delivered: false, relayed: 0 }
|
|
71
71
|
const tier = mailboxTierFromHop(hop)
|
|
72
72
|
const nodeHash = getNodeHash()
|
|
73
73
|
const record = {
|
|
74
|
-
...
|
|
74
|
+
...options.record,
|
|
75
75
|
toPubKeyHash,
|
|
76
76
|
hop,
|
|
77
77
|
tier,
|
|
78
|
-
fromNodeHash:
|
|
78
|
+
fromNodeHash: options.record?.fromNodeHash || nodeHash,
|
|
79
79
|
}
|
|
80
80
|
const stored = await storeMailboxRecord(record)
|
|
81
|
-
const toNodeHash =
|
|
81
|
+
const toNodeHash = options.toNodeHash?.trim().toLowerCase()
|
|
82
82
|
const delivered = toNodeHash && isMailboxRecordWithinSizeLimit(record)
|
|
83
83
|
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
84
84
|
: false
|
|
@@ -108,17 +108,17 @@ export async function publishMailboxRecord(username, toPubKeyHash, record, toNod
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* @param {{ replicaUsername?: string }}
|
|
111
|
+
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
112
112
|
* @param {object} put 入站 mailbox_put
|
|
113
113
|
* @param {string} [peerId] Trystero 对端 id(有则校验 nodeHash 绑定)
|
|
114
114
|
* @returns {Promise<void>}
|
|
115
115
|
*/
|
|
116
|
-
export async function ingestMailboxPut(
|
|
116
|
+
export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
117
117
|
const { record } = put
|
|
118
118
|
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
119
119
|
const fromNode = normalizeHex64(put.nodeHash)
|
|
120
120
|
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
121
|
-
const username = String(
|
|
121
|
+
const username = String(wireContext.replicaUsername || '').trim()
|
|
122
122
|
if (!username) return
|
|
123
123
|
if (peerId) {
|
|
124
124
|
const remote = await resolveRemoteNodeHashForPeer(username, peerId)
|
|
@@ -157,14 +157,14 @@ export async function respondMailboxWant(want, sendGive, peerId) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* @param {{ replicaUsername?: string }}
|
|
160
|
+
* @param {{ replicaUsername?: string }} wireContext 入站上下文
|
|
161
161
|
* @param {object} give mailbox_give 载荷
|
|
162
162
|
* @returns {Promise<number>} 投递给消费者的记录数
|
|
163
163
|
*/
|
|
164
|
-
export async function ingestMailboxGive(
|
|
164
|
+
export async function ingestMailboxGive(wireContext, give) {
|
|
165
165
|
const records = (give.records || []).filter(isDeliverableMailboxRecord)
|
|
166
166
|
if (!records.length) return 0
|
|
167
|
-
const username = String(
|
|
167
|
+
const username = String(wireContext.replicaUsername || '').trim()
|
|
168
168
|
if (!username) return 0
|
|
169
169
|
const { dispatchMailboxRecordsToConsumers } = await import('./consumer_registry.mjs')
|
|
170
170
|
const { deleteMailboxRecords } = await import('./store.mjs')
|
package/mailbox/importance.mjs
CHANGED
|
@@ -6,39 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
const TIER_ORDER = { quarantine: 0, normal: 1, trusted: 2 }
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* @param {number} score 信誉分
|
|
11
|
-
* @returns {MailboxTier} 分层
|
|
12
|
-
*/
|
|
13
|
-
export function mailboxTierFromScore(score) {
|
|
14
|
-
const numericScore = Number(score)
|
|
15
|
-
if (!Number.isFinite(numericScore)) return 'quarantine'
|
|
16
|
-
if (numericScore >= 0.45) return 'trusted'
|
|
17
|
-
if (numericScore >= 0.12) return 'normal'
|
|
18
|
-
return 'quarantine'
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {object} opts 参数
|
|
23
|
-
* @param {number} [opts.senderScore] 发件方信誉
|
|
24
|
-
* @param {number} [opts.recipientScore] 收件方关系信誉(可选)
|
|
25
|
-
* @param {boolean} [opts.knownMember] 是否本地已知成员/节点
|
|
26
|
-
* @param {number} [opts.hop] 转发跳数
|
|
27
|
-
* @returns {{ tier: MailboxTier, score: number }} 分层与分数
|
|
28
|
-
*/
|
|
29
|
-
export function scoreMailboxImportance(opts = {}) {
|
|
30
|
-
const sender = Number(opts.senderScore ?? 0)
|
|
31
|
-
const recipient = Number(opts.recipientScore ?? sender)
|
|
32
|
-
const known = !!opts.knownMember
|
|
33
|
-
const hop = Math.max(0, Number(opts.hop) || 0)
|
|
34
|
-
let score = sender * 0.65 + recipient * 0.35
|
|
35
|
-
if (known) score += 0.15
|
|
36
|
-
score -= hop * 0.08
|
|
37
|
-
if (score < 0) score = 0
|
|
38
|
-
if (score > 1) score = 1
|
|
39
|
-
return { tier: mailboxTierFromScore(score), score }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
9
|
/**
|
|
43
10
|
* 按 tier 与 storedAt 排序(低 tier 先淘汰)。
|
|
44
11
|
* @param {object[]} rows 记录
|
package/mailbox/settings.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getNodeTransportSettings } from '../node/identity.mjs'
|
|
2
1
|
import {
|
|
3
2
|
resolveMailboxRelayFanout,
|
|
4
3
|
resolveMailboxWantFanout,
|
|
@@ -43,11 +42,3 @@ export function resolveMailboxRoutingForPeerCount(peerCount, raw = {}, batterySa
|
|
|
43
42
|
batterySaver: true,
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @returns {{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }} mailbox 路由配置
|
|
49
|
-
*/
|
|
50
|
-
export function getMailboxRoutingSettings() {
|
|
51
|
-
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
52
|
-
return resolveMailboxRoutingForPeerCount(0, mailbox, batterySaver)
|
|
53
|
-
}
|
package/mailbox/wire.mjs
CHANGED
|
@@ -10,15 +10,15 @@ import { parseMailboxGive, parseMailboxPut, parseMailboxWant } from './parse.mjs
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @param {MailboxWireContext}
|
|
13
|
+
* @param {MailboxWireContext} wireContext 入站上下文
|
|
14
14
|
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
15
|
* @returns {void}
|
|
16
16
|
*/
|
|
17
|
-
export function attachMailboxWire(
|
|
17
|
+
export function attachMailboxWire(wireContext, wire) {
|
|
18
18
|
wire.on('mailbox_put', (payload, peerId) => {
|
|
19
19
|
const put = parseMailboxPut(payload)
|
|
20
20
|
if (!put.ok) return
|
|
21
|
-
void ingestMailboxPut(
|
|
21
|
+
void ingestMailboxPut(wireContext, put.value, peerId).catch(err => console.error('mailbox: put ingest failed', err))
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
wire.on('mailbox_want', (payload, peerId) => {
|
|
@@ -35,6 +35,6 @@ export function attachMailboxWire(ctx, wire) {
|
|
|
35
35
|
wire.on('mailbox_give', payload => {
|
|
36
36
|
const give = parseMailboxGive(payload)
|
|
37
37
|
if (!give.ok) return
|
|
38
|
-
void ingestMailboxGive(
|
|
38
|
+
void ingestMailboxGive(wireContext, give.value).catch(err => console.error('mailbox: give ingest failed', err))
|
|
39
39
|
})
|
|
40
40
|
}
|
package/node/denylist.mjs
CHANGED
|
@@ -56,13 +56,6 @@ function buildDenylistIndex(blocked) {
|
|
|
56
56
|
return index
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
* @returns {void}
|
|
61
|
-
*/
|
|
62
|
-
export function invalidateDenylistIndex() {
|
|
63
|
-
cachedIndex = null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
59
|
/**
|
|
67
60
|
* @returns {DenylistIndex} 缓存索引
|
|
68
61
|
*/
|
|
@@ -244,23 +237,6 @@ export async function addDenylistFromBanContent(banContent, groupId) {
|
|
|
244
237
|
await addDenylistEntry({ scope: 'subject', value: pk, ...sourceGroupId ? { groupId: sourceGroupId } : {} })
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
/**
|
|
248
|
-
* @param {string} entityHash 128 位十六进制
|
|
249
|
-
* @param {boolean} block true=拉黑
|
|
250
|
-
* @returns {Promise<boolean>} 当前是否拉黑
|
|
251
|
-
*/
|
|
252
|
-
export async function setEntityBlocked(entityHash, block) {
|
|
253
|
-
const id = String(entityHash || '').trim().toLowerCase()
|
|
254
|
-
if (!isEntityHash128(id)) throw new Error('invalid entityHash')
|
|
255
|
-
await mutateDenylist(() => {
|
|
256
|
-
const list = loadDenylist()
|
|
257
|
-
const without = list.blocked.filter(e => !(e.scope === 'entity' && e.value === id))
|
|
258
|
-
if (block) without.push({ scope: 'entity', value: id })
|
|
259
|
-
saveDenylist({ blocked: without })
|
|
260
|
-
})
|
|
261
|
-
return block
|
|
262
|
-
}
|
|
263
|
-
|
|
264
240
|
/**
|
|
265
241
|
* 追加群 scope 拉黑项。
|
|
266
242
|
* @param {string} groupId 群 ID
|
package/node/network.mjs
CHANGED
|
@@ -121,20 +121,6 @@ export function saveNetwork(data) {
|
|
|
121
121
|
invalidateTrustGraphCache()
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* @param {string} nodeHash 64 位十六进制
|
|
126
|
-
* @param {'trusted' | 'explore'} tier 池档位
|
|
127
|
-
* @returns {void}
|
|
128
|
-
*/
|
|
129
|
-
export function addNetworkPeer(nodeHash, tier = 'explore') {
|
|
130
|
-
const id = normalizeHex64(nodeHash)
|
|
131
|
-
if (!isHex64(id)) return
|
|
132
|
-
const net = loadNetwork()
|
|
133
|
-
const list = tier === 'trusted' ? net.trustedPeers : net.explorePeers
|
|
134
|
-
if (!list.includes(id)) list.push(id)
|
|
135
|
-
saveNetwork(net)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
124
|
/**
|
|
139
125
|
* @param {{ nodeHash: string, source: string, kind: string, weight?: number, expiresAt?: number, ttlMs?: number, groupId?: string }} hint 扩边 hint
|
|
140
126
|
* @returns {void}
|
|
@@ -165,35 +151,6 @@ export function applyNetworkHint(hint) {
|
|
|
165
151
|
saveNetwork(net)
|
|
166
152
|
}
|
|
167
153
|
|
|
168
|
-
/**
|
|
169
|
-
* @param {{ remoteNodeHash?: string }[]} roster Trystero roster
|
|
170
|
-
* @param {string} [groupId] 来源群
|
|
171
|
-
* @param {string} [source='roster'] hint 来源标签
|
|
172
|
-
* @returns {void}
|
|
173
|
-
*/
|
|
174
|
-
export function recordExplorePeersFromRoster(roster, groupId = '', source = 'roster') {
|
|
175
|
-
if (!roster?.length) return
|
|
176
|
-
const net = loadNetwork()
|
|
177
|
-
const now = Date.now()
|
|
178
|
-
for (const peer of roster) {
|
|
179
|
-
const nodeHash = normalizeHex64(peer?.remoteNodeHash)
|
|
180
|
-
if (!isHex64(nodeHash)) continue
|
|
181
|
-
if (!net.explorePeers.includes(nodeHash))
|
|
182
|
-
net.explorePeers.push(nodeHash)
|
|
183
|
-
net.hints.push({
|
|
184
|
-
nodeHash,
|
|
185
|
-
source: String(source || 'roster'),
|
|
186
|
-
kind: 'roster',
|
|
187
|
-
weight: 0.1,
|
|
188
|
-
expiresAt: now + 24 * 60 * 60 * 1000,
|
|
189
|
-
...groupId ? { groupId: String(groupId).trim() } : {},
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
net.hints = capHintsBySource(net.hints).slice(-MAX_HINTS)
|
|
193
|
-
net.lastRosterAt = now
|
|
194
|
-
saveNetwork(net)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
154
|
/**
|
|
198
155
|
* 疑似分区/eclipse 后:用 trusted 锚点加宽 explore,便于恢复联邦可达。
|
|
199
156
|
* @returns {void}
|
|
@@ -239,14 +196,6 @@ export function mergeNetworkPeerPools(patch = {}) {
|
|
|
239
196
|
saveNetwork(net)
|
|
240
197
|
}
|
|
241
198
|
|
|
242
|
-
/**
|
|
243
|
-
* @param {string[]} nodeHashes trusted 候选
|
|
244
|
-
* @returns {void}
|
|
245
|
-
*/
|
|
246
|
-
export function mergeTrustedPeers(nodeHashes) {
|
|
247
|
-
mergeNetworkPeerPools({ trustedPeers: nodeHashes })
|
|
248
|
-
}
|
|
249
|
-
|
|
250
199
|
/**
|
|
251
200
|
* @param {string} groupId 群 scope
|
|
252
201
|
* @param {'node' | 'subject' | 'entity'} scope denylist 作用域
|
package/node/personal_block.mjs
CHANGED
|
@@ -104,41 +104,6 @@ export async function loadPersonalBlockEntries(viewerEntityHash) {
|
|
|
104
104
|
return normalizePersonalListEntries(data?.blocked || [])
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
109
|
-
* @param {object} subject 待检主体
|
|
110
|
-
* @returns {Promise<boolean>} 是否被隐藏
|
|
111
|
-
*/
|
|
112
|
-
export async function isHiddenBy(viewerEntityHash, subject) {
|
|
113
|
-
return matchesPersonalListEntries(await loadPersonalHideEntries(viewerEntityHash), subject)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
118
|
-
* @param {object} subject 待检主体
|
|
119
|
-
* @returns {Promise<boolean>} 是否被拉黑
|
|
120
|
-
*/
|
|
121
|
-
export async function isBlockedBy(viewerEntityHash, subject) {
|
|
122
|
-
return matchesPersonalListEntries(await loadPersonalBlockEntries(viewerEntityHash), subject)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @param {string} viewerEntityHash 观看者实体
|
|
127
|
-
* @param {object} subject 待检主体
|
|
128
|
-
* @returns {Promise<boolean>} 是否应过滤
|
|
129
|
-
*/
|
|
130
|
-
export async function isFilteredByPersonalLists(viewerEntityHash, subject) {
|
|
131
|
-
if (!viewerEntityHash) return false
|
|
132
|
-
const [blocked, hidden, muted] = await Promise.all([
|
|
133
|
-
loadPersonalBlockEntries(viewerEntityHash),
|
|
134
|
-
loadPersonalHideEntries(viewerEntityHash),
|
|
135
|
-
loadPersonalMuteEntries(viewerEntityHash),
|
|
136
|
-
])
|
|
137
|
-
return matchesPersonalListEntries(blocked, subject)
|
|
138
|
-
|| matchesPersonalListEntries(hidden, subject)
|
|
139
|
-
|| matchesPersonalListEntries(muted, subject)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
107
|
/**
|
|
143
108
|
* @param {string} viewerEntityHash 本地可写实体
|
|
144
109
|
* @param {string} targetEntityHash 目标
|