@steve02081504/fount-p2p 0.0.0
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 +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
package/link/rtc.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
4
|
+
import { wrapRtcPeerConnectionForMdns } from '../transport/rtc_mdns_filter.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
|
|
8
|
+
* libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
|
|
9
|
+
* Windows 上会触发堆损坏(退出码 0xC0000374)。
|
|
10
|
+
*/
|
|
11
|
+
const { cleanup = undefined } = await import('node-datachannel').catch(console.error)
|
|
12
|
+
process.on('exit', () => {
|
|
13
|
+
try { cleanup?.() } catch { /* already torn down */ }
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 加载 node-datachannel polyfill,并按配置包装 RTCPeerConnection。
|
|
18
|
+
* @returns {Promise<{ RTCPeerConnection: typeof RTCPeerConnection, RTCIceCandidate: typeof RTCIceCandidate }>} RTC 构造器
|
|
19
|
+
*/
|
|
20
|
+
export async function loadNodeRtcPolyfill() {
|
|
21
|
+
const mod = await import('node-datachannel/polyfill')
|
|
22
|
+
const { mdnsPolicy } = getSignalingRuntimeConfig()
|
|
23
|
+
return {
|
|
24
|
+
RTCPeerConnection: wrapRtcPeerConnectionForMdns(mod.RTCPeerConnection, mod.RTCIceCandidate, mdnsPolicy),
|
|
25
|
+
RTCIceCandidate: mod.RTCIceCandidate,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 绑定 ICE candidate 回调,兼容 onicecandidate 与 onIceCandidate.subscribe。
|
|
31
|
+
* @param {RTCPeerConnection} pc 对等连接
|
|
32
|
+
* @param {(event: { candidate: RTCIceCandidate | null }) => void} handler candidate 事件处理器
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
export function attachIceCandidateListener(pc, handler) {
|
|
36
|
+
pc.onicecandidate = handler
|
|
37
|
+
pc.onIceCandidate?.subscribe?.(candidate =>
|
|
38
|
+
handler({ candidate: candidate ?? null })
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 绑定远端 data channel 回调,兼容 ondatachannel 与 onDataChannel.subscribe。
|
|
44
|
+
* @param {RTCPeerConnection} pc 对等连接
|
|
45
|
+
* @param {(event: { channel: RTCDataChannel }) => void} handler data channel 事件处理器
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
export function attachDataChannelListener(pc, handler) {
|
|
49
|
+
pc.ondatachannel = handler
|
|
50
|
+
pc.onDataChannel?.subscribe?.(channel => handler({ channel }))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 等待 data channel 进入 open 或 close 状态,超时则 reject。
|
|
55
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
56
|
+
* @param {'open' | 'close'} eventName 目标状态事件名
|
|
57
|
+
* @param {number} timeoutMs 超时毫秒数
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
*/
|
|
60
|
+
export function waitForChannelState(channel, eventName, timeoutMs) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
if (eventName === 'open' && channel.readyState === 'open') {
|
|
63
|
+
resolve()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
if (eventName === 'close' && channel.readyState === 'closed') {
|
|
67
|
+
resolve()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
cleanup()
|
|
72
|
+
reject(new Error(`p2p: data channel ${eventName} timeout after ${timeoutMs}ms`))
|
|
73
|
+
}, timeoutMs)
|
|
74
|
+
/**
|
|
75
|
+
* 通道状态变化处理函数。
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
const handler = () => {
|
|
79
|
+
cleanup()
|
|
80
|
+
resolve()
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 移除监听器并清除超时定时器。
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
clearTimeout(timer)
|
|
88
|
+
channel.removeEventListener?.(eventName, handler)
|
|
89
|
+
if (eventName === 'open' && channel.onopen === handler) channel.onopen = null
|
|
90
|
+
if (eventName === 'close' && channel.onclose === handler) channel.onclose = null
|
|
91
|
+
}
|
|
92
|
+
channel.addEventListener?.(eventName, handler)
|
|
93
|
+
if (eventName === 'open') channel.onopen = handler
|
|
94
|
+
if (eventName === 'close') channel.onclose = handler
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 将信令/消息数据统一转为 Uint8Array。
|
|
100
|
+
* @param {unknown} data 原始数据(字符串、ArrayBuffer、TypedArray 等)
|
|
101
|
+
* @returns {Uint8Array} 字节视图
|
|
102
|
+
*/
|
|
103
|
+
export function signalDataToBytes(data) {
|
|
104
|
+
if (data instanceof Uint8Array) return data
|
|
105
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data)
|
|
106
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
107
|
+
if (typeof data === 'string') return new TextEncoder().encode(data)
|
|
108
|
+
return new TextEncoder().encode(String(data ?? ''))
|
|
109
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 将 DTLS fingerprint 规范化为小写 `aa:bb:...` 格式。
|
|
3
|
+
* @param {unknown} value 原始 fingerprint 字符串
|
|
4
|
+
* @returns {string | null} 规范化后的 fingerprint,无效时返回 null
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeDtlsFingerprint(value) {
|
|
7
|
+
const text = String(value ?? '')
|
|
8
|
+
.trim()
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/^sha-256\s+/u, '')
|
|
11
|
+
if (!text) return null
|
|
12
|
+
const compact = text.replace(/\s+/gu, '')
|
|
13
|
+
if (!/^([\da-f]{2}:){31}[\da-f]{2}$/u.test(compact)) return null
|
|
14
|
+
return compact
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 从 SDP 文本中提取 SHA-256 DTLS fingerprint。
|
|
19
|
+
* @param {string} sdp SDP 描述字符串
|
|
20
|
+
* @returns {string | null} 规范化 fingerprint,未找到时返回 null
|
|
21
|
+
*/
|
|
22
|
+
export function extractDtlsFingerprint(sdp) {
|
|
23
|
+
const line = String(sdp || '').match(/^a=fingerprint:sha-256\s+([0-9A-Fa-f:]+)$/mu)?.[1]
|
|
24
|
+
return normalizeDtlsFingerprint(line)
|
|
25
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox 投递成功后由 Part 消费 envelope(P2P 不解析 DAG)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @typedef {(username: string, records: object[]) => Promise<string[]>} MailboxConsumer */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* handler: MailboxConsumer,
|
|
10
|
+
* }} MailboxConsumerEntry
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, MailboxConsumerEntry>} */
|
|
14
|
+
const consumers = new Map()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} app 应用名(与 record.app 匹配)
|
|
18
|
+
* @param {MailboxConsumer} handler 返回已交付 record id 列表
|
|
19
|
+
* @returns {void}
|
|
20
|
+
*/
|
|
21
|
+
export function registerMailboxConsumer(app, handler) {
|
|
22
|
+
consumers.set(String(app), { handler })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} app 应用名
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
export function unregisterMailboxConsumer(app) {
|
|
30
|
+
consumers.delete(String(app))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} username 副本用户名
|
|
35
|
+
* @param {object[]} records mailbox 记录
|
|
36
|
+
* @returns {Promise<string[]>} 所有 consumer 成功交付的 id 并集
|
|
37
|
+
*/
|
|
38
|
+
export async function dispatchMailboxRecordsToConsumers(username, records) {
|
|
39
|
+
/** @type {Map<string, object[]>} */
|
|
40
|
+
const grouped = new Map()
|
|
41
|
+
for (const row of records) {
|
|
42
|
+
const app = String(row?.app || '')
|
|
43
|
+
if (!app) continue
|
|
44
|
+
const bucket = grouped.get(app) || []
|
|
45
|
+
bucket.push(row)
|
|
46
|
+
grouped.set(app, bucket)
|
|
47
|
+
}
|
|
48
|
+
/** @type {Set<string>} */
|
|
49
|
+
const delivered = new Set()
|
|
50
|
+
for (const [app, { handler }] of consumers.entries()) {
|
|
51
|
+
const scoped = grouped.get(app)
|
|
52
|
+
if (!scoped?.length) continue
|
|
53
|
+
try {
|
|
54
|
+
const ids = await handler(username, scoped)
|
|
55
|
+
for (const id of ids || []) delivered.add(String(id))
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error('mailbox: consumer batch failed, retry per record', err)
|
|
59
|
+
for (const row of scoped)
|
|
60
|
+
try {
|
|
61
|
+
const ids = await handler(username, [row])
|
|
62
|
+
for (const id of ids || []) delivered.add(String(id))
|
|
63
|
+
}
|
|
64
|
+
catch (rowErr) {
|
|
65
|
+
console.error('mailbox: consumer record failed', rowErr)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...delivered]
|
|
71
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
|
+
import { deliverToUserRoomPeers, ensureUserRoom } from '../transport/user_room.mjs'
|
|
4
|
+
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
|
+
|
|
6
|
+
import { allowMailboxRelayForTier } from './importance.mjs'
|
|
7
|
+
import { takeIncomingMailboxPutSlot } from './rate.mjs'
|
|
8
|
+
import { resolveMailboxRoutingForPeerCount } from './settings.mjs'
|
|
9
|
+
import {
|
|
10
|
+
isDeliverableMailboxRecord,
|
|
11
|
+
isMailboxRecordWithinSizeLimit,
|
|
12
|
+
mailboxEnvelopeId,
|
|
13
|
+
mailboxTierFromHop,
|
|
14
|
+
normalizeMailboxHop,
|
|
15
|
+
relayHopAfterWireIngress,
|
|
16
|
+
storeMailboxRecord,
|
|
17
|
+
getMailboxRecords,
|
|
18
|
+
} from './store.mjs'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} username 副本用户名
|
|
22
|
+
* @param {string} peerId Trystero 对端 id
|
|
23
|
+
* @returns {Promise<string | null>} 已验证的 remote nodeHash
|
|
24
|
+
*/
|
|
25
|
+
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
|
+
if (!peerId) return null
|
|
27
|
+
const slot = await ensureUserRoom({ replicaUsername: username })
|
|
28
|
+
const entry = slot?.getRoster()?.find(row => row.peerId === peerId)
|
|
29
|
+
const remote = entry?.remoteNodeHash?.trim().toLowerCase()
|
|
30
|
+
return remote || null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} record 入站 record
|
|
35
|
+
* @returns {Promise<number>} 本节点应存储的 hop
|
|
36
|
+
*/
|
|
37
|
+
async function resolveRelayHopForIngress(record) {
|
|
38
|
+
let id
|
|
39
|
+
try {
|
|
40
|
+
id = mailboxEnvelopeId(record.envelope)
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return relayHopAfterWireIngress(record.hop)
|
|
44
|
+
}
|
|
45
|
+
const existing = (await getMailboxRecords([id]))[0]
|
|
46
|
+
return relayHopAfterWireIngress(record.hop, existing?.hop)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} username 副本用户名
|
|
51
|
+
* @returns {Promise<{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }>} 按在线 peer 数缩放的路由
|
|
52
|
+
*/
|
|
53
|
+
async function resolveRouting(username) {
|
|
54
|
+
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
55
|
+
const slot = await ensureUserRoom({ replicaUsername: username })
|
|
56
|
+
const peerCount = slot?.getRoster()?.length ?? 0
|
|
57
|
+
return resolveMailboxRoutingForPeerCount(peerCount, mailbox, batterySaver)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} username 副本用户名(trust graph 投递上下文)
|
|
62
|
+
* @param {object} opts 投递选项
|
|
63
|
+
* @returns {Promise<{ stored: boolean, delivered: boolean, relayed: number }>} 存转结果
|
|
64
|
+
*/
|
|
65
|
+
export async function deliverOrStoreMailboxPut(username, opts) {
|
|
66
|
+
const routing = await resolveRouting(username)
|
|
67
|
+
const toPubKeyHash = normalizeHex64(opts.toPubKeyHash)
|
|
68
|
+
if (!toPubKeyHash) return { stored: false, delivered: false, relayed: 0 }
|
|
69
|
+
const hop = normalizeMailboxHop(opts.hop)
|
|
70
|
+
if (hop >= routing.maxHop) return { stored: false, delivered: false, relayed: 0 }
|
|
71
|
+
const tier = mailboxTierFromHop(hop)
|
|
72
|
+
const nodeHash = getNodeHash()
|
|
73
|
+
const record = {
|
|
74
|
+
...opts.record,
|
|
75
|
+
toPubKeyHash,
|
|
76
|
+
hop,
|
|
77
|
+
tier,
|
|
78
|
+
fromNodeHash: opts.record?.fromNodeHash || nodeHash,
|
|
79
|
+
}
|
|
80
|
+
const stored = await storeMailboxRecord(record)
|
|
81
|
+
const toNodeHash = opts.toNodeHash?.trim().toLowerCase()
|
|
82
|
+
const delivered = toNodeHash && isMailboxRecordWithinSizeLimit(record)
|
|
83
|
+
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
84
|
+
: false
|
|
85
|
+
|
|
86
|
+
let relayed = 0
|
|
87
|
+
const relayFanout = tier === 'trusted' ? routing.relayFanoutTrusted : routing.relayFanoutNormal
|
|
88
|
+
if (stored && hop < routing.maxHop && allowMailboxRelayForTier(tier))
|
|
89
|
+
relayed = await deliverToUserRoomPeers(username, 'mailbox_put', { record }, null, relayFanout)
|
|
90
|
+
|
|
91
|
+
return { stored, delivered, relayed }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} username 副本用户名
|
|
96
|
+
* @param {string} toPubKeyHash 收件人
|
|
97
|
+
* @param {object} record 待发 record
|
|
98
|
+
* @param {string} [toNodeHash] 已知在线节点时直投
|
|
99
|
+
* @returns {Promise<{ stored: boolean, delivered: boolean, relayed: number }>} 存转结果
|
|
100
|
+
*/
|
|
101
|
+
export async function publishMailboxRecord(username, toPubKeyHash, record, toNodeHash = '') {
|
|
102
|
+
return deliverOrStoreMailboxPut(username, {
|
|
103
|
+
toPubKeyHash,
|
|
104
|
+
toNodeHash: toNodeHash || undefined,
|
|
105
|
+
record: { ...record, toPubKeyHash },
|
|
106
|
+
hop: 0,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {{ replicaUsername?: string }} ctx 入站上下文
|
|
112
|
+
* @param {object} put 入站 mailbox_put
|
|
113
|
+
* @param {string} [peerId] Trystero 对端 id(有则校验 nodeHash 绑定)
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
export async function ingestMailboxPut(ctx, put, peerId = '') {
|
|
117
|
+
const { record } = put
|
|
118
|
+
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
119
|
+
const fromNode = normalizeHex64(put.nodeHash)
|
|
120
|
+
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
121
|
+
const username = String(ctx?.replicaUsername || '').trim()
|
|
122
|
+
if (!username) return
|
|
123
|
+
if (peerId) {
|
|
124
|
+
const remote = await resolveRemoteNodeHashForPeer(username, peerId)
|
|
125
|
+
if (!remote || remote !== fromNode) return
|
|
126
|
+
}
|
|
127
|
+
const routing = await resolveRouting(username)
|
|
128
|
+
const relayHop = await resolveRelayHopForIngress(record)
|
|
129
|
+
if (relayHop >= routing.maxHop) return
|
|
130
|
+
await deliverOrStoreMailboxPut(username, {
|
|
131
|
+
toPubKeyHash: record.toPubKeyHash,
|
|
132
|
+
record: {
|
|
133
|
+
...record,
|
|
134
|
+
fromNodeHash: fromNode,
|
|
135
|
+
},
|
|
136
|
+
hop: relayHop,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {object} want mailbox_want 载荷
|
|
142
|
+
* @param {(payload: unknown, peerId: string) => void} sendGive mailbox_give 发送回调
|
|
143
|
+
* @param {string} peerId 请求方 peer
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
export async function respondMailboxWant(want, sendGive, peerId) {
|
|
147
|
+
const { getMailboxRecords, takeMailboxForRecipient } = await import('./store.mjs')
|
|
148
|
+
const recipient = normalizeHex64(want.toPubKeyHash)
|
|
149
|
+
if (!recipient) return
|
|
150
|
+
const ids = Array.isArray(want.ids) ? want.ids : []
|
|
151
|
+
const rows = (ids.length
|
|
152
|
+
? await getMailboxRecords(ids)
|
|
153
|
+
: await takeMailboxForRecipient(recipient)
|
|
154
|
+
).filter(row => row.toPubKeyHash === recipient && isDeliverableMailboxRecord(row))
|
|
155
|
+
if (!rows.length) return
|
|
156
|
+
sendGive({ toPubKeyHash: recipient, records: rows.slice(0, 32) }, peerId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {{ replicaUsername?: string }} ctx 入站上下文
|
|
161
|
+
* @param {object} give mailbox_give 载荷
|
|
162
|
+
* @returns {Promise<number>} 投递给消费者的记录数
|
|
163
|
+
*/
|
|
164
|
+
export async function ingestMailboxGive(ctx, give) {
|
|
165
|
+
const records = (give.records || []).filter(isDeliverableMailboxRecord)
|
|
166
|
+
if (!records.length) return 0
|
|
167
|
+
const username = String(ctx?.replicaUsername || '').trim()
|
|
168
|
+
if (!username) return 0
|
|
169
|
+
const { dispatchMailboxRecordsToConsumers } = await import('./consumer_registry.mjs')
|
|
170
|
+
const { deleteMailboxRecords } = await import('./store.mjs')
|
|
171
|
+
const delivered = await dispatchMailboxRecordsToConsumers(username, records)
|
|
172
|
+
if (delivered.length) await deleteMailboxRecords(delivered)
|
|
173
|
+
return delivered.length
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} username 副本用户名
|
|
178
|
+
* @param {string} toPubKeyHash 本机收件人 pubKeyHash
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
export async function requestMailboxFromNetwork(username, toPubKeyHash) {
|
|
182
|
+
const routing = await resolveRouting(username)
|
|
183
|
+
const { listMailboxIdsForRecipient } = await import('./store.mjs')
|
|
184
|
+
const recipient = normalizeHex64(toPubKeyHash)
|
|
185
|
+
if (!recipient) return
|
|
186
|
+
await deliverToUserRoomPeers(username, 'mailbox_want', {
|
|
187
|
+
toPubKeyHash: recipient,
|
|
188
|
+
ids: (await listMailboxIdsForRecipient(recipient)).slice(0, 64),
|
|
189
|
+
}, null, routing.wantFanout)
|
|
190
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox 重要性分层与评分(纯函数)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @typedef {'trusted' | 'normal' | 'quarantine'} MailboxTier */
|
|
6
|
+
|
|
7
|
+
const TIER_ORDER = { quarantine: 0, normal: 1, trusted: 2 }
|
|
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
|
+
/**
|
|
43
|
+
* 按 tier 与 storedAt 排序(低 tier 先淘汰)。
|
|
44
|
+
* @param {object[]} rows 记录
|
|
45
|
+
* @returns {object[]} 排序后
|
|
46
|
+
*/
|
|
47
|
+
export function sortMailboxForRetention(rows) {
|
|
48
|
+
return [...rows].sort((a, b) => {
|
|
49
|
+
const ta = TIER_ORDER[a.tier || 'normal'] ?? 1
|
|
50
|
+
const tb = TIER_ORDER[b.tier || 'normal'] ?? 1
|
|
51
|
+
if (ta !== tb) return ta - tb
|
|
52
|
+
return (a.storedAt || 0) - (b.storedAt || 0)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {MailboxTier} tier 分层
|
|
58
|
+
* @returns {number} 默认 TTL 毫秒
|
|
59
|
+
*/
|
|
60
|
+
export function defaultTtlMsForTier(tier) {
|
|
61
|
+
if (tier === 'trusted') return 30 * 24 * 3600 * 1000
|
|
62
|
+
if (tier === 'normal') return 7 * 24 * 3600 * 1000
|
|
63
|
+
return 24 * 3600 * 1000
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {MailboxTier} tier 分层
|
|
68
|
+
* @returns {boolean} 是否允许继续转发
|
|
69
|
+
*/
|
|
70
|
+
export function allowMailboxRelayForTier(tier) {
|
|
71
|
+
return tier !== 'quarantine'
|
|
72
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { assertHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import {
|
|
3
|
+
assertMailboxPubKeyHash,
|
|
4
|
+
assertMailboxRecordShape,
|
|
5
|
+
} from '../schemas/mailbox.mjs'
|
|
6
|
+
import { isPlainObject } from '../wire/ingress.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import('../schemas/mailbox.mjs').MailboxWireErr} MailboxParseErr
|
|
10
|
+
* @typedef {{ ok: true, value: object }} MailboxParseOk
|
|
11
|
+
* @typedef {MailboxParseOk | MailboxParseErr} MailboxParseResult
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown} payload 载荷
|
|
16
|
+
* @returns {MailboxParseResult} 解析结果
|
|
17
|
+
*/
|
|
18
|
+
export function parseMailboxPut(payload) {
|
|
19
|
+
if (!isPlainObject(payload))
|
|
20
|
+
return { ok: false, code: 'invalid_payload', field: 'payload' }
|
|
21
|
+
if (!isPlainObject(payload.record))
|
|
22
|
+
return { ok: false, code: 'required', field: 'record' }
|
|
23
|
+
const shape = assertMailboxRecordShape(payload.record)
|
|
24
|
+
if (!shape.ok) return shape
|
|
25
|
+
if (payload.nodeHash != null)
|
|
26
|
+
try {
|
|
27
|
+
assertHex64(payload.nodeHash, 'mailbox_put.nodeHash')
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { ok: false, code: 'invalid_hex64', field: 'nodeHash' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { ok: true, value: payload }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {unknown} payload 载荷
|
|
38
|
+
* @returns {MailboxParseResult} 解析结果
|
|
39
|
+
*/
|
|
40
|
+
export function parseMailboxWant(payload) {
|
|
41
|
+
if (!isPlainObject(payload))
|
|
42
|
+
return { ok: false, code: 'invalid_payload', field: 'payload' }
|
|
43
|
+
const pubKey = assertMailboxPubKeyHash(payload.toPubKeyHash)
|
|
44
|
+
if (!pubKey.ok)
|
|
45
|
+
return { ok: false, code: pubKey.code, field: 'toPubKeyHash' }
|
|
46
|
+
return { ok: true, value: { ...payload, toPubKeyHash: pubKey.value } }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {unknown} payload 载荷
|
|
51
|
+
* @returns {MailboxParseResult} 解析结果
|
|
52
|
+
*/
|
|
53
|
+
export function parseMailboxGive(payload) {
|
|
54
|
+
if (!isPlainObject(payload))
|
|
55
|
+
return { ok: false, code: 'invalid_payload', field: 'payload' }
|
|
56
|
+
if (!Array.isArray(payload.records))
|
|
57
|
+
return { ok: false, code: 'required', field: 'records' }
|
|
58
|
+
/** @type {object[]} */
|
|
59
|
+
const records = []
|
|
60
|
+
for (let i = 0; i < payload.records.length; i++) {
|
|
61
|
+
const record = payload.records[i]
|
|
62
|
+
const shape = assertMailboxRecordShape(record)
|
|
63
|
+
if (!shape.ok)
|
|
64
|
+
return { ok: false, code: shape.code, field: `records[${i}].${shape.field}` }
|
|
65
|
+
if (!record.envelope || typeof record.envelope !== 'object')
|
|
66
|
+
return { ok: false, code: 'required', field: `records[${i}].envelope` }
|
|
67
|
+
const app = String(record.app || '').trim()
|
|
68
|
+
if (!app)
|
|
69
|
+
return { ok: false, code: 'required', field: `records[${i}].app` }
|
|
70
|
+
records.push(record)
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, value: { ...payload, records } }
|
|
73
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mailbox 公平淘汰(纯函数,供 store 与单测复用)。
|
|
3
|
+
*/
|
|
4
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
|
|
6
|
+
/** 单收件人×发送者桶:条数上限 */
|
|
7
|
+
export const MAX_BUCKET_ENTRIES = 10
|
|
8
|
+
/** 单收件人×发送者桶:字节上限 */
|
|
9
|
+
export const MAX_BUCKET_BYTES = 2 * 1024 * 1024
|
|
10
|
+
/**
|
|
11
|
+
* 单用户 mailbox 全局条数上限。
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_MAILBOX_ENTRIES = 500
|
|
15
|
+
/**
|
|
16
|
+
* 单用户 mailbox 全局字节上限。
|
|
17
|
+
* @type {number}
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_MAILBOX_BYTES = 50 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} record 记录
|
|
23
|
+
* @returns {number} JSON 序列化字节量
|
|
24
|
+
*/
|
|
25
|
+
export function mailboxRecordBytes(record) {
|
|
26
|
+
return JSON.stringify(record).length
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} record 记录
|
|
31
|
+
* @returns {string} 公平淘汰桶键
|
|
32
|
+
*/
|
|
33
|
+
export function mailboxBucketKey(record) {
|
|
34
|
+
const to = normalizeHex64(record.toPubKeyHash) || ''
|
|
35
|
+
const from = record.fromNodeHash?.trim().toLowerCase()
|
|
36
|
+
|| record.envelope?.sender?.trim().toLowerCase()
|
|
37
|
+
|| 'unknown'
|
|
38
|
+
return `${to}\0${from}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {object[]} rows 记录列表
|
|
43
|
+
* @returns {object[]} 按桶修剪后
|
|
44
|
+
*/
|
|
45
|
+
export function pruneMailboxBuckets(rows) {
|
|
46
|
+
/** @type {Map<string, object[]>} */
|
|
47
|
+
const buckets = new Map()
|
|
48
|
+
for (const record of rows) {
|
|
49
|
+
const key = mailboxBucketKey(record)
|
|
50
|
+
const list = buckets.get(key) || []
|
|
51
|
+
list.push(record)
|
|
52
|
+
buckets.set(key, list)
|
|
53
|
+
}
|
|
54
|
+
/** @type {object[]} */
|
|
55
|
+
const out = []
|
|
56
|
+
for (const list of buckets.values()) {
|
|
57
|
+
const bucket = [...list].sort((a, b) => a.storedAt - b.storedAt)
|
|
58
|
+
let bytes = bucket.reduce((sum, record) => sum + mailboxRecordBytes(record), 0)
|
|
59
|
+
while (bucket.length > MAX_BUCKET_ENTRIES || bytes > MAX_BUCKET_BYTES) {
|
|
60
|
+
if (!bucket.length) break
|
|
61
|
+
const removed = bucket.shift()
|
|
62
|
+
bytes -= mailboxRecordBytes(removed)
|
|
63
|
+
}
|
|
64
|
+
out.push(...bucket)
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {object[]} rows 已按桶修剪后的记录
|
|
71
|
+
* @returns {object[]} 全局限额内
|
|
72
|
+
*/
|
|
73
|
+
export function pruneMailboxGlobalFair(rows) {
|
|
74
|
+
let kept = [...rows].sort((a, b) => a.storedAt - b.storedAt)
|
|
75
|
+
let totalBytes = kept.reduce((sum, record) => sum + mailboxRecordBytes(record), 0)
|
|
76
|
+
|
|
77
|
+
while ((kept.length > MAX_MAILBOX_ENTRIES || totalBytes > MAX_MAILBOX_BYTES) && kept.length > 1) {
|
|
78
|
+
/** @type {Map<string, object[]>} */
|
|
79
|
+
const buckets = new Map()
|
|
80
|
+
for (const record of kept) {
|
|
81
|
+
const key = mailboxBucketKey(record)
|
|
82
|
+
const list = buckets.get(key) || []
|
|
83
|
+
list.push(record)
|
|
84
|
+
buckets.set(key, list)
|
|
85
|
+
}
|
|
86
|
+
let victimKey = ''
|
|
87
|
+
let victimScore = -1
|
|
88
|
+
for (const [key, list] of buckets) {
|
|
89
|
+
const score = list.length * 1_000_000 + list.reduce((s, r) => s + mailboxRecordBytes(r), 0)
|
|
90
|
+
if (score > victimScore) {
|
|
91
|
+
victimScore = score
|
|
92
|
+
victimKey = key
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const victimList = buckets.get(victimKey) || []
|
|
96
|
+
if (!victimList.length) break
|
|
97
|
+
victimList.sort((a, b) => a.storedAt - b.storedAt)
|
|
98
|
+
const dropId = victimList[0].id
|
|
99
|
+
kept = kept.filter(record => record.id !== dropId)
|
|
100
|
+
totalBytes = kept.reduce((sum, record) => sum + mailboxRecordBytes(record), 0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (kept.length > MAX_MAILBOX_ENTRIES)
|
|
104
|
+
kept = kept.slice(-MAX_MAILBOX_ENTRIES)
|
|
105
|
+
return kept
|
|
106
|
+
}
|