@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.
Files changed (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. 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
+ }