@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,90 @@
1
+ /** @typedef {import('../wire/part_invoke.mjs').PartInvokeResponse} PartInvokeResponse */
2
+
3
+ /**
4
+ * @typedef {{
5
+ * replicaUsername?: string
6
+ * requesterNodeHash?: string | null
7
+ * groupId?: string
8
+ * peerId?: string
9
+ * }} InboundContext
10
+ */
11
+
12
+ /**
13
+ * @typedef {(ctx: InboundContext, message: object) => Promise<PartInvokeResponse | null>} RpcInboundHandler
14
+ */
15
+
16
+ /**
17
+ * @typedef {(ctx: InboundContext, message: object) => Promise<void>} DeliveryInboundHandler
18
+ */
19
+
20
+ /** @type {Map<string, RpcInboundHandler>} */
21
+ const rpcHandlers = new Map()
22
+
23
+ /** @type {Map<string, DeliveryInboundHandler>} */
24
+ const deliveryHandlers = new Map()
25
+
26
+ /**
27
+ * @param {string} type 入站 RPC 类型(part_invoke 等)
28
+ * @param {RpcInboundHandler} handler 处理器
29
+ * @returns {void}
30
+ */
31
+ export function registerRpcInboundHandler(type, handler) {
32
+ rpcHandlers.set(String(type || '').trim(), handler)
33
+ }
34
+
35
+ /**
36
+ * @param {string} type 入站投递类型(part_timeline_put 等)
37
+ * @param {DeliveryInboundHandler} handler 处理器
38
+ * @returns {void}
39
+ */
40
+ export function registerDeliveryInboundHandler(type, handler) {
41
+ deliveryHandlers.set(String(type || '').trim(), handler)
42
+ }
43
+
44
+ /**
45
+ * @param {string} type 入站类型
46
+ * @returns {void}
47
+ */
48
+ export function unregisterRpcInboundHandler(type) {
49
+ rpcHandlers.delete(String(type || '').trim())
50
+ }
51
+
52
+ /**
53
+ * @param {string} type 入站类型
54
+ * @returns {void}
55
+ */
56
+ export function unregisterDeliveryInboundHandler(type) {
57
+ deliveryHandlers.delete(String(type || '').trim())
58
+ }
59
+
60
+ /**
61
+ * @param {InboundContext} ctx 入站上下文
62
+ * @param {object} message 已校验的线载荷(含 type)
63
+ * @returns {Promise<PartInvokeResponse | null>} 处理器返回值
64
+ */
65
+ export async function dispatchRpcInbound(ctx, message) {
66
+ const type = String(message?.type || '').trim()
67
+ if (!type) return null
68
+ const handler = rpcHandlers.get(type)
69
+ if (!handler) return null
70
+ return handler(ctx, message)
71
+ }
72
+
73
+ /**
74
+ * @param {InboundContext} ctx 入站上下文
75
+ * @param {object} message 已校验的线载荷(含 type)
76
+ * @returns {Promise<void>}
77
+ */
78
+ export async function dispatchDeliveryInbound(ctx, message) {
79
+ const type = String(message?.type || '').trim()
80
+ if (!type) return
81
+ const handler = deliveryHandlers.get(type)
82
+ if (!handler) return
83
+ await handler(ctx, message)
84
+ }
85
+
86
+ /** @returns {void} 测试用 */
87
+ export function clearInboundHandlers() {
88
+ rpcHandlers.clear()
89
+ deliveryHandlers.clear()
90
+ }
@@ -0,0 +1,35 @@
1
+ /** @type {Map<string, (replicaUsername: string, groupId: string) => Promise<string>>} */
2
+ const groupMemberEntityByOwner = new Map()
3
+
4
+ /**
5
+ * @param {string} ownerId 注册方
6
+ * @param {(replicaUsername: string, groupId: string) => Promise<string>} resolver 群成员 entityHash
7
+ * @returns {void}
8
+ */
9
+ export function registerGroupMemberEntityResolver(ownerId, resolver) {
10
+ groupMemberEntityByOwner.set(String(ownerId), resolver)
11
+ }
12
+
13
+ /**
14
+ * @param {string} ownerId 注册方
15
+ * @returns {void}
16
+ */
17
+ export function unregisterGroupMemberEntityResolver(ownerId) {
18
+ groupMemberEntityByOwner.delete(String(ownerId))
19
+ }
20
+
21
+ /**
22
+ * @param {string} replicaUsername 副本用户名
23
+ * @param {string} groupId 群 ID
24
+ * @returns {Promise<string | null>} entityHash;无注册方时 null
25
+ */
26
+ export async function resolveGroupMemberEntityHash(replicaUsername, groupId) {
27
+ for (const resolver of groupMemberEntityByOwner.values())
28
+ try {
29
+ const eh = await resolver(replicaUsername, groupId)
30
+ if (eh) return eh
31
+ }
32
+ catch { /* next */ }
33
+
34
+ return null
35
+ }
@@ -0,0 +1,35 @@
1
+ import { normalizePartpath } from '../wire/part_invoke.mjs'
2
+
3
+ /** @type {Map<string, string>} shell 逻辑名 → partpath */
4
+ const shellPartpaths = new Map()
5
+
6
+ /**
7
+ * Shell Load 时注册本 part 的 partpath(P2P 层不硬编码 shells/*)。
8
+ * @param {string} shellKey 如 social、chat
9
+ * @param {string} partpath 如 shells/social
10
+ * @returns {void}
11
+ */
12
+ export function registerShellPartpath(shellKey, partpath) {
13
+ const key = String(shellKey || '').trim()
14
+ const path = normalizePartpath(partpath)
15
+ if (!key || !path) throw new Error('invalid_shell_partpath_registration')
16
+ shellPartpaths.set(key, path)
17
+ }
18
+
19
+ /**
20
+ * @param {string} shellKey 如 social、chat
21
+ * @returns {void}
22
+ */
23
+ export function unregisterShellPartpath(shellKey) {
24
+ shellPartpaths.delete(String(shellKey || '').trim())
25
+ }
26
+
27
+ /**
28
+ * @param {string} shellKey 如 social
29
+ * @returns {string} 已注册的 partpath
30
+ */
31
+ export function getShellPartpath(shellKey) {
32
+ const path = shellPartpaths.get(String(shellKey || '').trim())
33
+ if (!path) throw new Error(`shell partpath not registered: ${shellKey}`)
34
+ return path
35
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 联邦房间 Provider 注册表:P2P 层通过 Provider 获取群房间,不 import Chat Shell。
3
+ */
4
+
5
+ /**
6
+ * @typedef {{
7
+ * groupId: string
8
+ * getRoster: () => Array<{ peerId: string, remoteNodeHash?: string }>
9
+ * getPeerIdByNodeHash: (nodeHash: string) => string | null
10
+ * sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
11
+ * pickFallbackPeerIds?: (selfNodeHash: string) => Promise<string[]>
12
+ * }} FederationRoomSlot
13
+ */
14
+
15
+ /** @type {Map<string, (username: string) => FederationRoomSlot[] | Promise<FederationRoomSlot[]>>} */
16
+ const providers = new Map()
17
+
18
+ /**
19
+ * @param {string} ownerId 注册方(如 chat)
20
+ * @param {(username: string) => FederationRoomSlot[] | Promise<FederationRoomSlot[]>} enumerateRooms 枚举活跃房间
21
+ * @returns {void}
22
+ */
23
+ export function registerFederationRoomProvider(ownerId, enumerateRooms) {
24
+ providers.set(String(ownerId), enumerateRooms)
25
+ }
26
+
27
+ /**
28
+ * @param {string} ownerId 注册方
29
+ * @returns {void}
30
+ */
31
+ export function unregisterFederationRoomProvider(ownerId) {
32
+ providers.delete(String(ownerId))
33
+ }
34
+
35
+ /**
36
+ * @param {string} username 副本用户名 登录名
37
+ * @returns {Promise<FederationRoomSlot[]>} 所有 Provider 提供的活跃 sync 房间
38
+ */
39
+ export async function listFederationRoomSlots(username) {
40
+ /** @type {FederationRoomSlot[]} */
41
+ const slots = []
42
+ for (const enumerate of providers.values()) {
43
+ const batch = await enumerate(username)
44
+ if (batch?.length) slots.push(...batch)
45
+ }
46
+ return slots
47
+ }
@@ -0,0 +1,458 @@
1
+ /**
2
+ * 信誉纯内存算子(模拟器与磁盘 store 共用)。
3
+ */
4
+ import {
5
+ clampReputationScore,
6
+ computeRepMaxEff,
7
+ seedReputationFromIntro,
8
+ subjectiveSlashPenalty,
9
+ } from './math.mjs'
10
+ import { relayBumpIsDuplicate } from './relay_dedupe.mjs'
11
+ import reputationTunables from './tunables.json' with { type: 'json' }
12
+
13
+ /** @typedef {import('../node/reputation_store.mjs').ReputationFile} ReputationFile */
14
+
15
+ /**
16
+ * @returns {typeof reputationTunables} 默认 reputation tunables 副本
17
+ */
18
+ export function defaultReputationTunables() {
19
+ return reputationTunables
20
+ }
21
+
22
+ /**
23
+ * @param {ReputationFile} data 信誉表
24
+ * @param {string} nodeId 64 位十六进制
25
+ * @returns {{ score: number, offenseStreak?: number, lastOffenseAt?: number, blockPenalties?: Record<string, object> }} 节点行
26
+ */
27
+ function repRow(data, nodeId) {
28
+ const row = data.byNodeHash[nodeId] || { score: 0 }
29
+ const out = { score: Number(row.score ?? 0) }
30
+ if (Number.isFinite(row.offenseStreak))
31
+ out.offenseStreak = row.offenseStreak
32
+ if (Number.isFinite(row.lastOffenseAt))
33
+ out.lastOffenseAt = row.lastOffenseAt
34
+ if (row.blockPenalties) out.blockPenalties = row.blockPenalties
35
+ if (row.baseline) out.baseline = row.baseline
36
+ if (Number.isFinite(row.quarantinedUntil))
37
+ out.quarantinedUntil = row.quarantinedUntil
38
+ return out
39
+ }
40
+
41
+ /**
42
+ * @param {number} streak 连续作恶次数
43
+ * @param {typeof reputationTunables} tunables 参数
44
+ * @returns {number} 惩罚乘子 ≥ 1
45
+ */
46
+ export function computeRecidivismMultiplier(streak, tunables = reputationTunables) {
47
+ const step = Number(tunables.recidivismMultiplierStep ?? 0)
48
+ const maxMult = Number(tunables.recidivismMax ?? 1)
49
+ if (!Number.isFinite(streak) || streak <= 0 || step <= 0 || maxMult <= 1)
50
+ return 1
51
+ return Math.min(maxMult, 1 + step * streak)
52
+ }
53
+
54
+ /**
55
+ * @param {ReputationFile} data 信誉表
56
+ * @param {string} nodeId 64 位十六进制
57
+ * @returns {NonNullable<ReputationFile['byNodeHash'][string]>} 节点行(可变引用)
58
+ */
59
+ function ensureRow(data, nodeId) {
60
+ if (!data.byNodeHash[nodeId]) data.byNodeHash[nodeId] = { score: 0 }
61
+ return data.byNodeHash[nodeId]
62
+ }
63
+
64
+ /**
65
+ * @param {number} badCount 坏邀请计数
66
+ * @param {typeof reputationTunables} tunables 参数
67
+ * @returns {number} 惩罚乘子 ≥ 1
68
+ */
69
+ export function computeInviteEscalation(badCount, tunables = reputationTunables) {
70
+ const step = Number(tunables.inviteBadEscalationStep ?? 0)
71
+ const maxMult = Number(tunables.inviteBadEscalationMax ?? 1)
72
+ if (!Number.isFinite(badCount) || badCount <= 0 || step <= 0 || maxMult <= 1)
73
+ return 1
74
+ return Math.min(maxMult, 1 + step * badCount)
75
+ }
76
+
77
+ /**
78
+ * @param {object} row byNodeHash 行
79
+ * @param {number} gain 正向收益
80
+ * @param {typeof reputationTunables} tunables 参数
81
+ * @returns {void}
82
+ */
83
+ function applyRedemptionFromGain(row, gain, tunables = reputationTunables) {
84
+ if (!Number.isFinite(gain) || gain <= 0) return
85
+ const perStreak = Number(tunables.redemptionCreditPerStreakLevel ?? 0)
86
+ const perBad = Number(tunables.inviteRedemptionCreditPerBad ?? 0)
87
+ row.redemptionCredit = Number(row.redemptionCredit ?? 0) + gain
88
+ if (perStreak > 0) {
89
+ while ((row.offenseStreak ?? 0) > 0 && row.redemptionCredit >= perStreak) {
90
+ row.redemptionCredit -= perStreak
91
+ row.offenseStreak = Math.max(0, (row.offenseStreak ?? 0) - 1)
92
+ }
93
+ if ((row.offenseStreak ?? 0) <= 0) {
94
+ delete row.offenseStreak
95
+ delete row.lastOffenseAt
96
+ }
97
+ }
98
+ if (perBad > 0) {
99
+ while ((row.badInviteeCount ?? 0) > 0 && row.redemptionCredit >= perBad) {
100
+ row.redemptionCredit -= perBad
101
+ row.badInviteeCount = Math.max(0, (row.badInviteeCount ?? 0) - 1)
102
+ }
103
+ if ((row.badInviteeCount ?? 0) <= 0)
104
+ delete row.badInviteeCount
105
+ }
106
+ if (!Number.isFinite(row.redemptionCredit) || row.redemptionCredit <= 1e-9)
107
+ delete row.redemptionCredit
108
+ }
109
+
110
+ /**
111
+ * @param {ReputationFile} data 信誉表
112
+ * @param {string} nodeId 64 位十六进制
113
+ * @param {number} delta 增量
114
+ * @param {number} [now] 当前时间
115
+ * @param {typeof reputationTunables} [tunables] tunables
116
+ * @returns {void}
117
+ */
118
+ export function adjustNodeReputation(data, nodeId, delta, now = Date.now(), tunables = reputationTunables) {
119
+ const row = ensureRow(data, nodeId)
120
+ let effectiveDelta = delta
121
+ if (delta < 0) {
122
+ const streak = (row.offenseStreak ?? 0) + 1
123
+ row.offenseStreak = streak
124
+ row.lastOffenseAt = now
125
+ effectiveDelta = delta * computeRecidivismMultiplier(streak, tunables)
126
+ }
127
+ else if (delta > 0)
128
+ applyRedemptionFromGain(row, delta, tunables)
129
+
130
+ row.score = clampReputationScore(Number(row.score ?? 0) + effectiveDelta)
131
+ }
132
+
133
+ /**
134
+ * @param {ReputationFile} data 信誉表
135
+ * @param {string} nodeId 64 位十六进制
136
+ * @param {number} badDelta 坏邀请计数增量
137
+ * @returns {void}
138
+ */
139
+ export function incrementBadInviteeCount(data, nodeId, badDelta = 1) {
140
+ if (badDelta <= 0) return
141
+ const row = ensureRow(data, nodeId)
142
+ row.badInviteeCount = Math.max(0, Math.floor(Number(row.badInviteeCount ?? 0) + badDelta))
143
+ }
144
+
145
+ /**
146
+ * @param {ReputationFile} data 磁盘 JSON(可信)
147
+ * @returns {ReputationFile} 补齐字段后的同一对象
148
+ */
149
+ export function ensureReputationShape(data) {
150
+ data.byNodeHash ??= {}
151
+ data.wantUnknownHits ??= []
152
+ data.relayBumpSeen ??= []
153
+ return data
154
+ }
155
+
156
+ /**
157
+ * @param {ReputationFile} data 信誉表
158
+ * @param {typeof reputationTunables} [tunables] tunables
159
+ * @param {number} [now] 当前时间
160
+ * @returns {ReputationFile} 裁剪后的同一对象
161
+ */
162
+ export function pruneReputationFile(data, tunables = reputationTunables, now = Date.now()) {
163
+ data.wantUnknownHits = data.wantUnknownHits.filter(hit => now - hit.t <= tunables.wantUnknownWindowMs)
164
+ data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
165
+ if (data.relayBumpSeen.length > tunables.maxRelayBumpSeen)
166
+ data.relayBumpSeen = data.relayBumpSeen.slice(-tunables.maxRelayBumpSeen)
167
+ return data
168
+ }
169
+
170
+ /**
171
+ * @param {ReputationFile} data 信誉表
172
+ * @param {string} peerNodeHash 对端节点
173
+ * @param {string} [dedupeKey] 去重键
174
+ * @param {number} [now] 当前时间
175
+ * @param {typeof reputationTunables} [tunables] tunables
176
+ * @returns {boolean} 是否已加分
177
+ */
178
+ export function bumpReputationOnRelayPure(data, peerNodeHash, dedupeKey, now = Date.now(), tunables = reputationTunables) {
179
+ const id = String(peerNodeHash || '').trim()
180
+ if (!id) return false
181
+ const key = String(dedupeKey || `conn:${id}`).trim()
182
+ data.relayBumpSeen = data.relayBumpSeen.filter(hit => now - hit.t <= tunables.relayBumpDedupeMs)
183
+ if (relayBumpIsDuplicate(data.relayBumpSeen, id, key, now, tunables.relayBumpDedupeMs)) return false
184
+ data.relayBumpSeen.push({ peerNodeHash: id, key, t: now })
185
+ adjustNodeReputation(data, id, tunables.relayRepBump, now, tunables)
186
+ return true
187
+ }
188
+
189
+ /**
190
+ * @param {ReputationFile} data 信誉表
191
+ * @param {string} peerNodeHash 请求方
192
+ * @param {number} [now] 当前时间
193
+ * @param {typeof reputationTunables} [tunables] tunables
194
+ * @returns {boolean} 是否触发惩罚
195
+ */
196
+ export function recordGossipAllUnknownWantPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
197
+ data.wantUnknownHits = data.wantUnknownHits.filter(h => now - h.t <= tunables.wantUnknownWindowMs)
198
+ data.wantUnknownHits.push({ peerNodeHash, t: now })
199
+ const recent = data.wantUnknownHits.filter(h => h.peerNodeHash === peerNodeHash)
200
+ if (recent.length >= tunables.wantUnknownThreshold) {
201
+ adjustNodeReputation(data, peerNodeHash, -tunables.penaltyUnknownWant)
202
+ data.wantUnknownHits = data.wantUnknownHits.filter(h => h.peerNodeHash !== peerNodeHash)
203
+ return true
204
+ }
205
+ return false
206
+ }
207
+
208
+ /**
209
+ * @param {ReputationFile} data 信誉表
210
+ * @param {string} peerNodeHash 对端
211
+ * @param {typeof reputationTunables} [tunables] tunables
212
+ * @param {number} [excessRatio=1] 超速超额比例 0..1
213
+ * @returns {void}
214
+ */
215
+ export function recordMessageRateViolationPure(data, peerNodeHash, tunables = reputationTunables, excessRatio = 1) {
216
+ const id = String(peerNodeHash || '').trim()
217
+ if (!id) return
218
+ const ratio = Math.max(0, Math.min(1, Number(excessRatio) || 0))
219
+ if (ratio <= 0) return
220
+ adjustNodeReputation(data, id, -tunables.penaltyMessageRate * ratio)
221
+ }
222
+
223
+ /**
224
+ * @param {ReputationFile} data 信誉表
225
+ * @param {string} storagePeerKey 责任方
226
+ * @param {typeof reputationTunables} [tunables] tunables
227
+ * @returns {void}
228
+ */
229
+ export function bumpChunkStorageReputationPure(data, storagePeerKey, tunables = reputationTunables) {
230
+ const id = String(storagePeerKey || '').trim()
231
+ if (!id) return
232
+ adjustNodeReputation(data, id, tunables.chunkStoreRepBump)
233
+ }
234
+
235
+ /**
236
+ * @param {ReputationFile} data 信誉表
237
+ * @param {string} blamePeerKey 责任方
238
+ * @param {typeof reputationTunables} [tunables] tunables
239
+ * @returns {void}
240
+ */
241
+ export function penalizeChunkStorageFailurePure(data, blamePeerKey, tunables = reputationTunables) {
242
+ const id = String(blamePeerKey || '').trim()
243
+ if (!id) return
244
+ adjustNodeReputation(data, id, -tunables.chunkFetchFailPenalty)
245
+ }
246
+
247
+ /**
248
+ * @param {ReputationFile} data 信誉表
249
+ * @param {string} peerNodeHash 对端 nodeHash
250
+ * @param {typeof reputationTunables} [tunables] tunables
251
+ * @returns {void}
252
+ */
253
+ export function penalizeArchiveServeMismatchPure(data, peerNodeHash, tunables = reputationTunables) {
254
+ const id = String(peerNodeHash || '').trim()
255
+ if (!id) return
256
+ adjustNodeReputation(data, id, -tunables.archiveServeMismatchPenalty)
257
+ }
258
+
259
+ /**
260
+ * @param {ReputationFile} data 信誉表
261
+ * @param {string} target 目标 nodeHash
262
+ * @param {string} sender 发送方 nodeHash
263
+ * @param {number} claim 主张强度
264
+ * @param {boolean} verified 是否可验证
265
+ * @param {typeof reputationTunables} [tunables] tunables
266
+ * @returns {void}
267
+ */
268
+ export function applySubjectiveSlashPure(data, target, sender, claim, verified, tunables = reputationTunables) {
269
+ const repMaxEff = computeRepMaxEff(data)
270
+ const repSender = Number(data.byNodeHash[sender]?.score ?? 0)
271
+ const penalty = subjectiveSlashPenalty(claim, repSender, repMaxEff, verified, tunables)
272
+ adjustNodeReputation(data, target, -penalty)
273
+ }
274
+
275
+ /**
276
+ * @param {ReputationFile} data 信誉表
277
+ * @param {string} targetPubKeyHash 目标
278
+ * @param {Array<{ from?: string, to?: string }>} inviteEdges 邀请边
279
+ * @param {typeof reputationTunables} [tunables] tunables
280
+ * @returns {Array<{ hop: number, node: string, dRep: number }>} 已应用的上游惩罚
281
+ */
282
+ export function applyDecayCollusionAfterSlashPure(data, targetPubKeyHash, inviteEdges, tunables = reputationTunables) {
283
+ const target = targetPubKeyHash.trim().toLowerCase()
284
+ /** @type {Array<{ hop: number, node: string, dRep: number }>} */
285
+ const applied = []
286
+ let frontier = new Set([target])
287
+ let maxBadSeen = 0
288
+ const baseMaxHop = Number(tunables.collusionMaxHop ?? 6)
289
+ const hopBonusEvery = Math.max(1, Number(tunables.inviteHopBonusEvery ?? 2))
290
+ const hopBonusMax = Number(tunables.inviteHopBonusMax ?? 0)
291
+ const deltaBoostPerBad = Number(tunables.inviteDeltaBoostPerBad ?? 0)
292
+ const deltaBoostMax = Number(tunables.inviteDeltaBoostMax ?? 0)
293
+
294
+ /**
295
+ * @returns {number} 当前有效最大跳数
296
+ */
297
+ function effectiveMaxHop() {
298
+ return baseMaxHop + Math.min(hopBonusMax, Math.floor(maxBadSeen / hopBonusEvery))
299
+ }
300
+
301
+ for (let hop = 1; hop <= effectiveMaxHop(); hop++) {
302
+ const upstream = new Set()
303
+ for (const edge of inviteEdges) {
304
+ const to = String(edge.to ?? '').trim().toLowerCase()
305
+ const from = String(edge.from ?? '').trim().toLowerCase()
306
+ if (frontier.has(to) && from) upstream.add(from)
307
+ }
308
+ if (!upstream.size) break
309
+
310
+ for (const node of upstream) {
311
+ incrementBadInviteeCount(data, node, 1)
312
+ const row = ensureRow(data, node)
313
+ const badCount = Number(row.badInviteeCount ?? 0)
314
+ maxBadSeen = Math.max(maxBadSeen, badCount)
315
+ const boostedDelta = Math.min(1, Number(tunables.collusionDelta ?? 0.62) + Math.min(deltaBoostMax, deltaBoostPerBad * badCount))
316
+ const baseDRep = Number(tunables.collusionLambda ?? 0.07) * boostedDelta ** hop
317
+ const dRep = baseDRep * computeInviteEscalation(badCount, tunables)
318
+ const before = Number(row.score ?? 0)
319
+ adjustNodeReputation(data, node, -dRep, Date.now(), tunables)
320
+ const after = Number(data.byNodeHash[node]?.score ?? 0)
321
+ applied.push({ hop, node, dRep: before - after })
322
+ }
323
+ frontier = upstream
324
+ }
325
+ return applied
326
+ }
327
+
328
+ /**
329
+ * @param {ReputationFile} data 信誉表
330
+ * @param {string} targetPubKeyHash 目标
331
+ * @returns {void}
332
+ */
333
+ export function applyReputationResetToScoresPure(data, targetPubKeyHash) {
334
+ const t = targetPubKeyHash.trim().toLowerCase()
335
+ const row = repRow(data, t)
336
+ row.score = 0
337
+ delete row.offenseStreak
338
+ delete row.lastOffenseAt
339
+ delete row.badInviteeCount
340
+ delete row.redemptionCredit
341
+ data.byNodeHash[t] = row
342
+ }
343
+
344
+ /**
345
+ * @param {ReputationFile} data 信誉表
346
+ * @param {string} memberPubKeyHash 新成员
347
+ * @param {string} [introducerPubKeyHash] 介绍者
348
+ * @param {number} [repEdge] 边信任;省略则用 tunable introducerSeedEdge
349
+ * @param {typeof reputationTunables} [tunables] tunables
350
+ * @param {number} [powBonus=0] 入群 PoW 自愿封顶加成
351
+ * @returns {void}
352
+ */
353
+ export function seedMemberReputationFromIntroducerPure(data, memberPubKeyHash, introducerPubKeyHash, repEdge, tunables = reputationTunables, powBonus = 0) {
354
+ const memberKey = memberPubKeyHash.trim().toLowerCase()
355
+ const introducerKey = introducerPubKeyHash?.trim().toLowerCase() || ''
356
+ if (data.byNodeHash[memberKey]) return
357
+ const introducerReputation = introducerKey
358
+ ? Number(data.byNodeHash[introducerKey]?.score ?? 0)
359
+ : 0
360
+ data.byNodeHash[memberKey] = { score: seedReputationFromIntro(introducerReputation, repEdge, tunables, powBonus) }
361
+ }
362
+
363
+ /**
364
+ * @param {object | null | undefined} groupSettings 群设置
365
+ * @param {typeof reputationTunables} [tunables] tunables
366
+ * @returns {number} 毫秒
367
+ */
368
+ export function resolveSlashAlertTtlMsPure(groupSettings, tunables = reputationTunables) {
369
+ const n = Number(groupSettings?.slashAlertTtl)
370
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : tunables.defaultSlashAlertTtlMs
371
+ }
372
+
373
+ /**
374
+ * @param {object} row byNodeHash 行
375
+ * @returns {{ mean: number, m2: number, count: number }} 行为基线
376
+ */
377
+ function baselineRow(row) {
378
+ const baseline = row.baseline || { mean: 0, m2: 0, count: 0 }
379
+ return {
380
+ mean: Number(baseline.mean ?? 0),
381
+ m2: Number(baseline.m2 ?? 0),
382
+ count: Number(baseline.count ?? 0),
383
+ }
384
+ }
385
+
386
+ /**
387
+ * @param {ReputationFile} data 信誉表
388
+ * @param {string} peerNodeHash 对端
389
+ * @param {number} sample 观测值
390
+ * @param {number} [now] 当前时间
391
+ * @param {typeof reputationTunables} [tunables] tunables
392
+ * @returns {{ z: number, anomaly: boolean }} 偏离度
393
+ */
394
+ export function observeBehaviorSamplePure(data, peerNodeHash, sample, now = Date.now(), tunables = reputationTunables) {
395
+ const id = String(peerNodeHash || '').trim()
396
+ if (!id) return { z: 0, anomaly: false }
397
+ const row = repRow(data, id)
398
+ const baseline = baselineRow(row)
399
+ const alpha = Number(tunables.baselineAlpha ?? 0.08)
400
+ const value = Number(sample)
401
+ if (!Number.isFinite(value)) return { z: 0, anomaly: false }
402
+
403
+ const prevMean = baseline.mean
404
+ const prevCount = baseline.count
405
+ const newCount = prevCount + 1
406
+ const newMean = prevCount === 0 ? value : prevMean + alpha * (value - prevMean)
407
+ const diff = value - newMean
408
+ const newM2 = prevCount === 0 ? 0 : (1 - alpha) * (baseline.m2 + alpha * (value - prevMean) ** 2)
409
+ row.baseline = { mean: newMean, m2: newM2, count: newCount }
410
+ data.byNodeHash[id] = row
411
+
412
+ const variance = newCount > 1 ? Math.max(newM2, 1e-6) : 0
413
+ const std = Math.sqrt(variance)
414
+ const z = std > 0 ? Math.abs(value - newMean) / std : 0
415
+ const minSamples = Number(tunables.baselineMinSamples ?? 6)
416
+ const anomaly = newCount >= minSamples && z >= Number(tunables.anomalyZThreshold ?? 2.8)
417
+ if (anomaly)
418
+ applyQuarantinePure(data, id, now, tunables)
419
+ return { z, anomaly }
420
+ }
421
+
422
+ /**
423
+ * @param {ReputationFile} data 信誉表
424
+ * @param {string} peerNodeHash 对端
425
+ * @param {number} [now] 当前时间
426
+ * @param {typeof reputationTunables} [tunables] tunables
427
+ * @returns {void}
428
+ */
429
+ export function applyQuarantinePure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
430
+ const id = String(peerNodeHash || '').trim()
431
+ if (!id) return
432
+ const row = repRow(data, id)
433
+ row.quarantinedUntil = now + Number(tunables.quarantineTtlMs ?? 900_000)
434
+ data.byNodeHash[id] = row
435
+ }
436
+
437
+ /**
438
+ * @param {ReputationFile} data 信誉表
439
+ * @param {string} peerNodeHash 对端
440
+ * @param {number} [now] 当前时间
441
+ * @returns {boolean} 是否处于本地隔离
442
+ */
443
+ export function isQuarantinedPure(data, peerNodeHash, now = Date.now()) {
444
+ const id = String(peerNodeHash || '').trim()
445
+ const until = Number(data.byNodeHash?.[id]?.quarantinedUntil ?? 0)
446
+ return Number.isFinite(until) && until > now
447
+ }
448
+
449
+ /**
450
+ * @param {ReputationFile} data 信誉表
451
+ * @param {string} peerNodeHash 对端
452
+ * @param {number} [now] 当前时间
453
+ * @param {typeof reputationTunables} [tunables] tunables
454
+ * @returns {boolean} 是否检测到异常并触发隔离
455
+ */
456
+ export function detectAnomalyPure(data, peerNodeHash, now = Date.now(), tunables = reputationTunables) {
457
+ return isQuarantinedPure(data, peerNodeHash, now)
458
+ }