@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
@@ -0,0 +1,73 @@
1
+ import reputationTunables from './tunables.json' with { type: 'json' }
2
+
3
+ /**
4
+ * 主观信誉标量下界(§0.3)。
5
+ * @type {number}
6
+ */
7
+ export const REP_MIN = -1
8
+ /**
9
+ * 主观信誉标量上界(§0.3)。
10
+ * @type {number}
11
+ */
12
+ export const REP_MAX = 1
13
+ /** §0.1:`rep_max_eff = max(已链邻居最大信誉, ε)` */
14
+ export const REP_MAX_EFF_EPS = 1e-12
15
+
16
+ /**
17
+ * @param {number} x 任意标量
18
+ * @returns {number} clamp 到 [-1, 1]
19
+ */
20
+ export function clampReputationScore(x) {
21
+ return Math.min(REP_MAX, Math.max(REP_MIN, x))
22
+ }
23
+
24
+ /**
25
+ * @param {{ byNodeHash?: Record<string, { score?: number }> }} data 信誉表
26
+ * @returns {number} `max(已链邻居最大信誉, ε)`(§0.1 `rep_max_eff`)
27
+ */
28
+ export function computeRepMaxEff(data) {
29
+ let maxScore = /** @type {number | null} */ null
30
+ for (const nodeId of Object.keys(data.byNodeHash)) {
31
+ const score = Number(data.byNodeHash[nodeId]?.score)
32
+ if (!Number.isFinite(score)) continue
33
+ // rep_max_eff 仅由「可施加影响」的正信誉邻居定义,避免全负信誉集把分母压成异常值。
34
+ if (score <= 0) continue
35
+ maxScore = maxScore === null ? score : Math.max(maxScore, score)
36
+ }
37
+ return Math.max(maxScore === null ? 0 : clampReputationScore(maxScore), REP_MAX_EFF_EPS)
38
+ }
39
+
40
+ /**
41
+ * 不可验证 Slash 落地扣分(§0.1)。
42
+ * @param {number} claim 主张强度
43
+ * @param {number} repSender 发送方信誉
44
+ * @param {number} repMaxEff 分母
45
+ * @param {boolean} [verified] 是否可验证
46
+ * @param {typeof reputationTunables} [tunables] tunables
47
+ * @returns {number} 对目标的扣分幅度(正数)
48
+ */
49
+ export function subjectiveSlashPenalty(claim, repSender, repMaxEff, verified = false, tunables = reputationTunables) {
50
+ const claimStrength = Number.isFinite(claim) ? claim : tunables.slashDefaultClaim
51
+ const senderInfluence = Math.max(0, Number.isFinite(repSender) ? repSender : 0)
52
+ return verified
53
+ ? Math.abs(claimStrength) * tunables.slashVerifiedMultiplier
54
+ : Math.abs((claimStrength * senderInfluence) / repMaxEff)
55
+ }
56
+
57
+ /**
58
+ * §0.3 初值:`clamp(rep_local(intro) * reputationEdge)`。
59
+ *
60
+ * `repEdge` 缺省时退回 tunable `introducerSeedEdge`(抗女巫杠杆:边信任越低,
61
+ * 新成员从介绍者继承的初始信誉越少,邀请链批量灌号的收益越小)。
62
+ * @param {number} introRep 介绍者信誉
63
+ * @param {number} [repEdge] 边信任;省略则用 tunable 默认
64
+ * @param {typeof reputationTunables} [tunables] tunables
65
+ * @param {number} [powBonus=0] 入群 PoW 自愿封顶加成
66
+ * @returns {number} 新成员初值
67
+ */
68
+ export function seedReputationFromIntro(introRep, repEdge, tunables = reputationTunables, powBonus = 0) {
69
+ const edge = Number.isFinite(repEdge) ? repEdge : tunables.introducerSeedEdge
70
+ const base = introRep * (Number.isFinite(edge) ? clampReputationScore(edge) : 1)
71
+ const bonus = Number.isFinite(powBonus) && powBonus > 0 ? powBonus : 0
72
+ return clampReputationScore(base + bonus)
73
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 纯函数:从已加载的 reputation 文件体计算节点全局分(无 setting_loader 依赖)。
3
+ * @param {object} repFile reputation.json 内容
4
+ * @param {string} nodeId 64 位十六进制 节点
5
+ * @returns {number} 信誉分
6
+ */
7
+ export function pickNodeScoreFromReputation(repFile, nodeId) {
8
+ const row = repFile?.byNodeHash?.[nodeId]
9
+ if (!row) return 0
10
+ return Number(row.score ?? 0)
11
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 联邦中继信誉加分去重(纯函数)。
3
+ */
4
+ import reputationTunables from './tunables.json' with { type: 'json' }
5
+
6
+ /**
7
+ * @param {Array<{ peerNodeHash: string, key: string, t: number }>} relayBumpSeen 已记录贡献
8
+ * @param {string} peerNodeHash 对端节点
9
+ * @param {string} dedupeKey 去重键
10
+ * @param {number} [now] 当前时间
11
+ * @param {number} [dedupeMs] 去重窗口
12
+ * @returns {boolean} 24h 内已计过分则为 true
13
+ */
14
+ export function relayBumpIsDuplicate(relayBumpSeen, peerNodeHash, dedupeKey, now = Date.now(), dedupeMs = reputationTunables.relayBumpDedupeMs) {
15
+ if (!peerNodeHash) return true
16
+ const dedupe = dedupeKey || `conn:${peerNodeHash}`
17
+ return relayBumpSeen.some(
18
+ entry => entry.peerNodeHash === peerNodeHash
19
+ && entry.key === dedupe
20
+ && now - entry.t <= dedupeMs,
21
+ )
22
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "wantUnknownWindowMs": 300000,
3
+ "wantUnknownThreshold": 3,
4
+ "penaltyUnknownWant": 0.12,
5
+ "penaltyMessageRate": 0.15,
6
+ "chunkStoreRepBump": 0.03,
7
+ "chunkFetchFailPenalty": 0.08,
8
+ "archiveServeMismatchPenalty": 0.08,
9
+ "relayRepBump": 0.02,
10
+ "defaultSlashAlertTtlMs": 86400000,
11
+ "maxRelayBumpSeen": 2000,
12
+ "relayBumpDedupeMs": 86400000,
13
+ "collusionLambda": 0.07,
14
+ "collusionDelta": 0.62,
15
+ "collusionMaxHop": 6,
16
+ "slashVerifiedMultiplier": 0.5,
17
+ "slashDefaultClaim": 0.2,
18
+ "slashVerifiedDefaultClaim": 0.35,
19
+ "slashUnverifiedDefaultClaim": 0.2,
20
+ "introducerSeedEdge": 0.5,
21
+ "recidivismMultiplierStep": 0.25,
22
+ "recidivismMax": 3,
23
+ "redemptionCreditPerStreakLevel": 0.08,
24
+ "inviteRedemptionCreditPerBad": 0.15,
25
+ "inviteBadEscalationStep": 0.5,
26
+ "inviteBadEscalationMax": 4,
27
+ "inviteHopBonusEvery": 2,
28
+ "inviteHopBonusMax": 4,
29
+ "inviteDeltaBoostPerBad": 0.05,
30
+ "inviteDeltaBoostMax": 0.3,
31
+ "baselineAlpha": 0.08,
32
+ "anomalyZThreshold": 2.8,
33
+ "quarantineTtlMs": 900000,
34
+ "quarantineTrustDamp": 0.35,
35
+ "baselineMinSamples": 6
36
+ }
@@ -0,0 +1,185 @@
1
+ import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
2
+ import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
3
+ import {
4
+ groupRendezvousTopic,
5
+ decryptSignalPacket,
6
+ encryptSignalPacket,
7
+ getLinkRegistry,
8
+ } from '../transport/link_registry.mjs'
9
+
10
+ /**
11
+ * 在指定 scope 与 roomSecret 下创建 link 层房间(discovery + registry 转发)。
12
+ * @param {object} opts 房间选项
13
+ * @param {string} opts.scope link registry scope(如 `group:{id}`)
14
+ * @param {string} opts.roomSecret 房间 rendezvous 密钥
15
+ * @param {(nodeHash: string) => boolean} [opts.allowNode] 是否允许与某 nodeHash 通信
16
+ * @returns {{ start: () => Promise<void>, leave: () => Promise<void>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], onPeerJoin: (cb: (peerId: string) => void) => () => void, onPeerLeave: (cb: (peerId: string) => void) => () => void, getPeers: () => Record<string, true> }} 房间句柄
17
+ */
18
+ export function createScopedLinkRoom(opts) {
19
+ const registry = getLinkRegistry()
20
+ const scope = String(opts.scope)
21
+ const topic = groupRendezvousTopic(opts.roomSecret)
22
+ const allowNode = typeof opts.allowNode === 'function' ? opts.allowNode : () => true
23
+ /** @type {Set<string>} */
24
+ const discoveredPeers = new Set()
25
+ /** @type {Set<string>} */
26
+ const announcedPeers = new Set()
27
+ /** @type {Set<(peerId: string) => void>} */
28
+ const joinListeners = new Set()
29
+ /** @type {Set<(peerId: string) => void>} */
30
+ const leaveListeners = new Set()
31
+ /** @type {Set<() => void>} */
32
+ const cleanups = new Set()
33
+ /** @type {Map<string, { handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }>} */
34
+ const actionEntries = new Map()
35
+
36
+ /**
37
+ * @returns {string[]} 当前已连接且通过 allowNode 过滤的 peer nodeHash 列表
38
+ */
39
+ function activePeerIds() {
40
+ return [...discoveredPeers].filter(nodeHash => allowNode(nodeHash) && registry.getLink(nodeHash))
41
+ }
42
+
43
+ /**
44
+ * @param {Set<(peerId: string) => void>} listeners 回调集合
45
+ * @param {string} peerId 节点 hash
46
+ * @returns {void}
47
+ */
48
+ function emit(listeners, peerId) {
49
+ for (const listener of listeners)
50
+ try { listener(peerId) } catch { /* ignore */ }
51
+ }
52
+
53
+ /**
54
+ * @param {string} peerId 节点 hash
55
+ * @returns {void}
56
+ */
57
+ function notePeerJoin(peerId) {
58
+ if (!peerId || announcedPeers.has(peerId)) return
59
+ announcedPeers.add(peerId)
60
+ emit(joinListeners, peerId)
61
+ }
62
+
63
+ /**
64
+ * @param {string} peerId 节点 hash
65
+ * @returns {void}
66
+ */
67
+ function notePeerLeave(peerId) {
68
+ if (!peerId || !announcedPeers.has(peerId)) return
69
+ announcedPeers.delete(peerId)
70
+ emit(leaveListeners, peerId)
71
+ }
72
+
73
+ /**
74
+ * @param {string} name action 名称
75
+ * @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 槽(含待处理 backlog)
76
+ */
77
+ function getActionEntry(name) {
78
+ const key = String(name)
79
+ if (!actionEntries.has(key))
80
+ actionEntries.set(key, { handler: null, backlog: [] })
81
+ return actionEntries.get(key)
82
+ }
83
+
84
+ return {
85
+ /**
86
+ * @returns {Promise<void>}
87
+ */
88
+ async start() {
89
+ await registry.ensureRuntime()
90
+ cleanups.add(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
91
+ if (!allowNode(senderNodeHash)) return
92
+ const entry = actionEntries.get(String(envelope?.action || ''))
93
+ if (!entry) return
94
+ if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
95
+ else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
96
+ }))
97
+ cleanups.add(registry.onLinkUp(nodeHash => {
98
+ if (!discoveredPeers.has(nodeHash) || !allowNode(nodeHash)) return
99
+ notePeerJoin(nodeHash)
100
+ }))
101
+ cleanups.add(registry.onLinkDown(nodeHash => {
102
+ if (!discoveredPeers.has(nodeHash)) return
103
+ notePeerLeave(nodeHash)
104
+ }))
105
+ cleanups.add(await subscribeTopic(topic, async bytes => {
106
+ const packet = decryptSignalPacket(topic, bytes)
107
+ if (packet?.type !== 'advert' || !packet.body) return
108
+ const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
109
+ if (!verifiedNodeHash || !allowNode(verifiedNodeHash)) return
110
+ discoveredPeers.add(verifiedNodeHash)
111
+ await registry.ensureLinkToNode(verifiedNodeHash).catch(() => null)
112
+ if (registry.getLink(verifiedNodeHash)) notePeerJoin(verifiedNodeHash)
113
+ }))
114
+ cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
115
+ type: 'advert',
116
+ body: await buildSignedAdvert(topic, Date.now(), registry.localIdentity),
117
+ })))
118
+ for (const peerId of activePeerIds())
119
+ notePeerJoin(peerId)
120
+ },
121
+ /**
122
+ * @returns {Promise<void>}
123
+ */
124
+ async leave() {
125
+ for (const cleanup of cleanups)
126
+ try { cleanup() } catch { /* ignore */ }
127
+ cleanups.clear()
128
+ for (const peerId of [...announcedPeers])
129
+ notePeerLeave(peerId)
130
+ },
131
+ /**
132
+ * @param {string} name action 名称
133
+ * @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} [send, onReceive] 发送与订阅元组
134
+ */
135
+ makeAction(name) {
136
+ const actionName = String(name)
137
+ return [
138
+ async (payload, peerId = null) => {
139
+ if (Array.isArray(peerId)) {
140
+ await Promise.all(peerId.map(targetPeerId =>
141
+ registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })))
142
+ return
143
+ }
144
+ if (peerId)
145
+ await registry.sendToNodeLink(peerId, { scope, action: actionName, payload })
146
+ else
147
+ await Promise.all(activePeerIds().map(targetPeerId =>
148
+ registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })))
149
+ },
150
+ handler => {
151
+ const entry = getActionEntry(actionName)
152
+ entry.handler = handler
153
+ for (const pending of entry.backlog.splice(0))
154
+ handler(pending.payload, pending.peerId)
155
+ },
156
+ ]
157
+ },
158
+ /**
159
+ * @param {(peerId: string) => void} cb 新 peer 上线回调
160
+ * @returns {() => void} 取消订阅
161
+ */
162
+ onPeerJoin(cb) {
163
+ joinListeners.add(cb)
164
+ for (const peerId of activePeerIds())
165
+ announcedPeers.add(peerId)
166
+ for (const peerId of announcedPeers)
167
+ try { cb(peerId) } catch { /* ignore */ }
168
+ return () => joinListeners.delete(cb)
169
+ },
170
+ /**
171
+ * @param {(peerId: string) => void} cb peer 离线回调
172
+ * @returns {() => void} 取消订阅
173
+ */
174
+ onPeerLeave(cb) {
175
+ leaveListeners.add(cb)
176
+ return () => leaveListeners.delete(cb)
177
+ },
178
+ /**
179
+ * @returns {Record<string, true>} 当前活跃 peer 的 nodeHash 集合
180
+ */
181
+ getPeers() {
182
+ return Object.fromEntries(activePeerIds().map(peerId => [peerId, true]))
183
+ },
184
+ }
185
+ }
@@ -0,0 +1,110 @@
1
+ import { assertHex64, isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { isPlainObject } from '../wire/ingress.mjs'
3
+
4
+ const MAX_DISCOVERY_ADS = 64
5
+
6
+ /**
7
+ * @param {unknown} ad 单条 discovery 广告
8
+ * @returns {object | null} 白名单字段
9
+ */
10
+ function sanitizeDiscoveryAdvertisement(ad) {
11
+ if (!isPlainObject(ad)) return null
12
+ const groupId = String(ad.groupId || '').trim()
13
+ const advertiserPubKeyHash = normalizeHex64(ad.advertiserPubKeyHash)
14
+ const signature = String(ad.signature || '').trim().toLowerCase()
15
+ if (!groupId || !isHex64(advertiserPubKeyHash) || !isSignatureHex128(signature)) return null
16
+ const advertiserNodeHash = normalizeHex64(ad.advertiserNodeHash)
17
+ const body = {
18
+ groupId,
19
+ title: String(ad.title || '').slice(0, 200),
20
+ blurb: String(ad.blurb || '').slice(0, 500),
21
+ advertiserPubKeyHash,
22
+ advertiserNodeHash: isHex64(advertiserNodeHash) ? advertiserNodeHash : String(ad.advertiserNodeHash || '').trim(),
23
+ observedAt: Number(ad.observedAt) || 0,
24
+ signature,
25
+ }
26
+ const memberCount = Number(ad.memberCount)
27
+ if (Number.isFinite(memberCount) && memberCount > 0)
28
+ body.memberCount = Math.floor(memberCount)
29
+ return body
30
+ }
31
+
32
+ /**
33
+ * @param {unknown} ads advertisements 数组
34
+ * @returns {object[]} 已清扫广告
35
+ */
36
+ function sanitizeDiscoveryAdvertisements(ads) {
37
+ if (!Array.isArray(ads)) return []
38
+ return ads.slice(0, MAX_DISCOVERY_ADS).map(sanitizeDiscoveryAdvertisement).filter(Boolean)
39
+ }
40
+
41
+ /**
42
+ * @param {unknown} nodeHash 节点 hash
43
+ * @returns {string} 规范化 hex64
44
+ */
45
+ export function assertDiscoveryNodeHash(nodeHash) {
46
+ return assertHex64(nodeHash, 'discovery.nodeHash')
47
+ }
48
+
49
+ /**
50
+ * @param {unknown} requestId 请求 id
51
+ * @returns {string} 非空 trimmed 字符串
52
+ */
53
+ export function assertDiscoveryRequestId(requestId) {
54
+ const id = String(requestId ?? '').trim()
55
+ if (!id) throw new Error('discovery.requestId required')
56
+ return id
57
+ }
58
+
59
+ /**
60
+ * @param {unknown} payload 载荷
61
+ * @returns {{ nodeHash: string, advertisements: object[] } | null} 解析结果
62
+ */
63
+ export function parseDiscoveryAnnounce(payload) {
64
+ if (!isPlainObject(payload)) return null
65
+ try {
66
+ return {
67
+ nodeHash: assertDiscoveryNodeHash(payload.nodeHash),
68
+ advertisements: sanitizeDiscoveryAdvertisements(payload.advertisements),
69
+ }
70
+ }
71
+ catch {
72
+ return null
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @param {unknown} payload 载荷
78
+ * @returns {{ nodeHash: string, requestId: string, limit: number } | null} 解析结果
79
+ */
80
+ export function parseDiscoveryQuery(payload) {
81
+ if (!isPlainObject(payload)) return null
82
+ try {
83
+ return {
84
+ nodeHash: assertDiscoveryNodeHash(payload.nodeHash),
85
+ requestId: assertDiscoveryRequestId(payload.requestId),
86
+ limit: Math.min(64, Math.max(1, Number(payload.limit) || 32)),
87
+ }
88
+ }
89
+ catch {
90
+ return null
91
+ }
92
+ }
93
+
94
+ /**
95
+ * @param {unknown} payload 载荷
96
+ * @returns {{ requestId: string, nodeHash: string, advertisements: object[] } | null} 解析结果
97
+ */
98
+ export function parseDiscoveryQueryResponse(payload) {
99
+ if (!isPlainObject(payload)) return null
100
+ try {
101
+ return {
102
+ requestId: assertDiscoveryRequestId(payload.requestId),
103
+ nodeHash: assertDiscoveryNodeHash(payload.nodeHash),
104
+ advertisements: sanitizeDiscoveryAdvertisements(payload.advertisements),
105
+ }
106
+ }
107
+ catch {
108
+ return null
109
+ }
110
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 联邦补拉 attestation / HPKE 响应 wire 解析(无 attestation/envelope 即丢弃)。
3
+ */
4
+ import { isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
5
+ import { EVENT_ID_HEX } from '../dag/index.mjs'
6
+ import { isPlainObject } from '../wire/ingress.mjs'
7
+
8
+ /** @typedef {{ requesterPubKeyHash: string, groupId: string, requestId: string, timestamp: number, wantIds?: string[], signature: string }} PullAttestation */
9
+
10
+ const ENVELOPE_BLOB_MIN_LEN = 2
11
+
12
+ /**
13
+ * @param {unknown} value 密文字段
14
+ * @returns {boolean} 是否像合法 ECIES blob 段
15
+ */
16
+ function isEnvelopeBlob(value) {
17
+ return String(value ?? '').trim().length >= ENVELOPE_BLOB_MIN_LEN
18
+ }
19
+
20
+ /**
21
+ * @param {unknown} attestation 载荷 attestation 字段
22
+ * @returns {PullAttestation | null} 解析结果
23
+ */
24
+ export function parsePullAttestation(attestation) {
25
+ if (!isPlainObject(attestation)) return null
26
+ const requesterPubKeyHash = normalizeHex64(attestation.requesterPubKeyHash)
27
+ const groupId = String(attestation.groupId || '').trim()
28
+ const requestId = String(attestation.requestId || '').trim()
29
+ const timestamp = Number(attestation.timestamp)
30
+ const signature = String(attestation.signature || '').trim().toLowerCase()
31
+ if (!isHex64(requesterPubKeyHash) || !groupId || !Number.isFinite(timestamp) || !isSignatureHex128(signature))
32
+ return null
33
+ const wantIds = Array.isArray(attestation.wantIds)
34
+ ? [...new Set(attestation.wantIds.map(id => String(id).trim().toLowerCase()).filter(id => EVENT_ID_HEX.test(id)))]
35
+ : undefined
36
+ return {
37
+ requesterPubKeyHash,
38
+ groupId,
39
+ requestId,
40
+ timestamp,
41
+ wantIds,
42
+ signature,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * @param {unknown} envelope 响应 envelope
48
+ * @returns {{ requestId: string, requesterPubKeyHash: string, requesterNodeHash: string, ephemPub: string, iv: string, ciphertext: string, authTag: string } | null} 解析结果
49
+ */
50
+ export function parsePullResponseEnvelope(envelope) {
51
+ if (!isPlainObject(envelope)) return null
52
+ const requestId = String(envelope.requestId || '').trim()
53
+ const requesterPubKeyHash = normalizeHex64(envelope.requesterPubKeyHash)
54
+ const requesterNodeHash = String(envelope.requesterNodeHash || '').trim()
55
+ const ephemPub = String(envelope.ephemPub || '').trim()
56
+ const iv = String(envelope.iv || '').trim()
57
+ const ciphertext = String(envelope.ciphertext || '').trim()
58
+ const authTag = String(envelope.authTag || '').trim()
59
+ if (!requestId || !isHex64(requesterPubKeyHash) || !requesterNodeHash) return null
60
+ if (!isEnvelopeBlob(ephemPub) || !isEnvelopeBlob(iv) || !isEnvelopeBlob(ciphertext) || !isEnvelopeBlob(authTag))
61
+ return null
62
+ return {
63
+ requestId,
64
+ requesterPubKeyHash,
65
+ requesterNodeHash,
66
+ ephemPub,
67
+ iv,
68
+ ciphertext,
69
+ authTag,
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {unknown} data 入群快照请求载荷
75
+ * @returns {object | null} 解析结果
76
+ */
77
+ export function parseJoinSnapshotRequest(data) {
78
+ if (!isPlainObject(data)) return null
79
+ const requestId = String(data.requestId || '').trim()
80
+ const requesterNodeHash = String(data.requesterNodeHash || '').trim()
81
+ const groupId = String(data.groupId || '').trim()
82
+ const attestation = parsePullAttestation(data.attestation)
83
+ if (!requestId || !requesterNodeHash || !groupId || !attestation) return null
84
+ if (attestation.groupId !== groupId || attestation.requestId !== requestId) return null
85
+ const tipsHash = String(data.tipsHash || '').trim()
86
+ return {
87
+ requestId,
88
+ requesterNodeHash,
89
+ requesterPubKeyHash: attestation.requesterPubKeyHash,
90
+ groupId,
91
+ tipsHash: tipsHash ? tipsHash.toLowerCase() : undefined,
92
+ attestation,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {unknown} data 入群快照响应载荷
98
+ * @returns {object | null} 解析结果
99
+ */
100
+ export function parseJoinSnapshotResponse(data) {
101
+ return parsePullResponseEnvelope(data)
102
+ }
@@ -0,0 +1,37 @@
1
+ import { normalizeHex64, HEX_ID_64 } from '../core/hexIds.mjs'
2
+
3
+ /**
4
+ * @typedef {{ ok: true, value: string }} MailboxWireOk
5
+ * @typedef {{ ok: false, code: string, field: string }} MailboxWireErr
6
+ * @typedef {MailboxWireOk | MailboxWireErr} MailboxWireResult
7
+ */
8
+
9
+ /**
10
+ * @typedef {{ ok: true, value: object }} MailboxRecordShapeOk
11
+ * @typedef {MailboxWireErr} MailboxRecordShapeErr
12
+ * @typedef {MailboxRecordShapeOk | MailboxRecordShapeErr} MailboxRecordShapeResult
13
+ */
14
+
15
+ /**
16
+ * @param {unknown} value 收件人 pubKeyHash
17
+ * @returns {MailboxWireResult} 规范化 hex64 或结构化错误
18
+ */
19
+ export function assertMailboxPubKeyHash(value) {
20
+ const normalized = normalizeHex64(value)
21
+ if (!HEX_ID_64.test(normalized))
22
+ return { ok: false, code: 'invalid_hex64', field: 'toPubKeyHash' }
23
+ return { ok: true, value: normalized }
24
+ }
25
+
26
+ /**
27
+ * @param {unknown} record mailbox 记录
28
+ * @returns {MailboxRecordShapeResult} 已校验的 record 或结构化错误
29
+ */
30
+ export function assertMailboxRecordShape(record) {
31
+ if (!record || typeof record !== 'object')
32
+ return { ok: false, code: 'required', field: 'record' }
33
+ const pubKey = assertMailboxPubKeyHash(record.toPubKeyHash)
34
+ if (!pubKey.ok)
35
+ return { ok: false, code: pubKey.code, field: 'record.toPubKeyHash' }
36
+ return { ok: true, value: record }
37
+ }
@@ -0,0 +1,17 @@
1
+ import { assertHex64 } from '../core/hexIds.mjs'
2
+
3
+ /**
4
+ * 远程入站事件形状校验(仅 federation / P2P 路径调用)。
5
+ * @param {object} event DAG 事件
6
+ * @returns {void}
7
+ */
8
+ export function validateRemoteEventShape(event) {
9
+ if (!event?.type || typeof event.type !== 'string')
10
+ throw new Error('remote event: type required')
11
+ assertHex64(event.id, 'remote event.id')
12
+ if (!Array.isArray(event.prev_event_ids))
13
+ throw new Error('remote event: prev_event_ids must be array')
14
+ for (const parentId of event.prev_event_ids)
15
+ assertHex64(parentId, 'remote event.prev_event_id')
16
+ assertHex64(event.sender, 'remote event.sender')
17
+ }
@@ -0,0 +1,54 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ import { nextHlc } from '../core/hlc.mjs'
4
+ import { publicKeyFromSeed, sign } from '../crypto/crypto.mjs'
5
+ import {
6
+ computeEventId,
7
+ eventBodyForSign,
8
+ signPayloadBytes,
9
+ sortedPrevEventIds,
10
+ } from '../dag/index.mjs'
11
+ import { computeDagTipIdsFromEvents } from '../governance/branch.mjs'
12
+
13
+ /**
14
+ * 为追加事件计算 HLC 与 DAG 前驱(chat 群与 social 时间线共用)。
15
+ * @param {object[]} previous 已有事件
16
+ * @param {object} event 待追加事件
17
+ * @param {{ multiTip?: boolean }} [opts] chat 在多 tip 时连接全部 tip
18
+ * @returns {{ hlc: object, prev_event_ids: string[], last: object | undefined }} HLC、前驱与末条事件
19
+ */
20
+ export function computeAppendHlcAndPrev(previous, event, opts = {}) {
21
+ const last = previous[previous.length - 1]
22
+ const hlc = nextHlc(last?.hlc, event.timestamp ?? Date.now())
23
+ const tips = computeDagTipIdsFromEvents(previous)
24
+ let prev_event_ids
25
+ if (event.prev_event_ids?.length)
26
+ prev_event_ids = sortedPrevEventIds(event.prev_event_ids)
27
+ else if (opts.multiTip && tips.length > 1)
28
+ prev_event_ids = sortedPrevEventIds(tips)
29
+ else if (tips.length)
30
+ prev_event_ids = sortedPrevEventIds(tips)
31
+ else if (last?.id)
32
+ prev_event_ids = [last.id]
33
+ else
34
+ prev_event_ids = []
35
+ return { hlc, prev_event_ids, last }
36
+ }
37
+
38
+ /**
39
+ * 使用 eventBodyForSign 规范签名并返回完整事件(social 时间线)。
40
+ * @param {object} base 事件基体(含 type/groupId/sender/hlc/prev_event_ids/content/node_id)
41
+ * @param {Uint8Array} secretKey 签名密钥
42
+ * @returns {object} 签名后事件
43
+ */
44
+ export async function signTimelineEvent(base, secretKey) {
45
+ const body = eventBodyForSign(base)
46
+ const id = computeEventId(body)
47
+ const signature = await sign(signPayloadBytes(body), secretKey)
48
+ return {
49
+ ...base,
50
+ id,
51
+ signature: Buffer.from(signature).toString('hex'),
52
+ senderPubKey: Buffer.from(publicKeyFromSeed(secretKey)).toString('hex'),
53
+ }
54
+ }
@@ -0,0 +1,26 @@
1
+ import { topologicalCanonicalOrder } from '../dag/index.mjs'
2
+
3
+ /**
4
+ * 拓扑序后按 reducer 表折叠事件列表。
5
+ * @param {object[]} events 原始事件
6
+ * @param {Record<string, (state: object, event: object) => object>} reducers 事件类型 → reducer
7
+ * @param {() => object} createInitialState 初始状态工厂
8
+ * @returns {{ state: object, order: string[] }} 物化结果与拓扑序
9
+ */
10
+ export function materializeFromEvents(events, reducers, createInitialState) {
11
+ const order = topologicalCanonicalOrder(events.map(event => ({
12
+ id: event.id,
13
+ prev_event_ids: event.prev_event_ids,
14
+ hlc: event.hlc,
15
+ node_id: event.node_id,
16
+ })))
17
+ const byId = new Map(events.map(event => [event.id, event]))
18
+ let state = createInitialState()
19
+ for (const eventId of order) {
20
+ const event = byId.get(eventId)
21
+ if (!event) continue
22
+ const reducer = reducers[event.type]
23
+ if (reducer) state = reducer(state, event)
24
+ }
25
+ return { state, order }
26
+ }