@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,124 @@
1
+ /**
2
+ * DAG 拓扑序磁盘缓存(events.order.json):增量合并,避免每次全量 Kahn。
3
+ */
4
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
5
+ import { dirname } from 'node:path'
6
+
7
+ import {
8
+ computeLocalTipsHash,
9
+ eventsToMetas,
10
+ sortedPrevEventIds,
11
+ topologicalCanonicalOrder,
12
+ } from '../dag/index.mjs'
13
+ import { computeDagTipIdsFromEvents } from '../governance/branch.mjs'
14
+
15
+ /**
16
+ * 将新增事件按父指针插入已有拓扑序之后。
17
+ * @param {string[]} cachedOrder 已缓存序
18
+ * @param {object[]} events 全量事件
19
+ * @returns {string[]} 合并后的 id 序
20
+ */
21
+ export function mergeTopologicalOrder(cachedOrder, events) {
22
+ const byId = new Map(events.map(event => [event.id, event]))
23
+ const merged = cachedOrder.filter(id => byId.has(id))
24
+ const orderSet = new Set(merged)
25
+ const newEvents = events.filter(event => !orderSet.has(event.id))
26
+ if (!newEvents.length) return merged
27
+
28
+ const newOrder = topologicalCanonicalOrder(eventsToMetas(newEvents))
29
+ for (const id of newOrder) {
30
+ if (orderSet.has(id)) continue
31
+ const event = byId.get(id)
32
+ let insertAt = merged.length
33
+ for (const parentId of sortedPrevEventIds(event?.prev_event_ids)) {
34
+ const parentIdx = merged.indexOf(parentId)
35
+ if (parentIdx >= 0) insertAt = Math.max(insertAt, parentIdx + 1)
36
+ }
37
+ merged.splice(insertAt, 0, id)
38
+ orderSet.add(id)
39
+ }
40
+ return merged
41
+ }
42
+
43
+ /**
44
+ * @param {object[]} events 全量事件
45
+ * @param {object | null} cache 磁盘缓存
46
+ * @param {{ forceFull?: boolean }} [opts] 选项
47
+ * @returns {string[]} 拓扑序 id 列表
48
+ */
49
+ export function resolveEventTopologicalOrder(events, cache, opts = {}) {
50
+ if (!events.length) return []
51
+ if (opts.forceFull)
52
+ return topologicalCanonicalOrder(eventsToMetas(events))
53
+
54
+ const byId = new Map(events.map(event => [event.id, event]))
55
+ const tips = computeDagTipIdsFromEvents(events)
56
+ const tipsHash = computeLocalTipsHash(tips)
57
+ const eventCount = events.length
58
+
59
+ if (cache && Array.isArray(cache.order) && cache.order.length) {
60
+ if (cache.tipsHash === tipsHash && cache.eventCount === eventCount) {
61
+ const ok = cache.order.length === eventCount && cache.order.every(id => byId.has(id))
62
+ if (ok) return cache.order
63
+ }
64
+ if (cache.eventCount <= eventCount && cache.order.every(id => byId.has(id))) {
65
+ const merged = mergeTopologicalOrder(cache.order, events)
66
+ if (merged.length === eventCount) return merged
67
+ }
68
+ }
69
+
70
+ return topologicalCanonicalOrder(eventsToMetas(events))
71
+ }
72
+
73
+ /**
74
+ * @param {string[]} order 拓扑序
75
+ * @param {object[]} events 全量事件
76
+ * @returns {{ order: string[], tipsHash: string, eventCount: number }} 可写入 events.order.json
77
+ */
78
+ export function buildOrderCachePayload(order, events) {
79
+ const tips = computeDagTipIdsFromEvents(events)
80
+ return {
81
+ order,
82
+ tipsHash: computeLocalTipsHash(tips),
83
+ eventCount: events.length,
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string} path `events.order.json` 路径
89
+ * @returns {Promise<object | null>} 缓存或 null
90
+ */
91
+ export async function readOrderCache(path) {
92
+ try {
93
+ return JSON.parse(await readFile(path, 'utf8'))
94
+ }
95
+ catch {
96
+ return null
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 写入拓扑序缓存。纯性能缓存:写失败(如群目录在并发清理/拆群时被删,Windows 上 open 报
102
+ * EPERM、或 ENOENT/EBUSY)不应让物化抛错或上浮告警,静默忽略即可,下次读盘自会重建。
103
+ * @param {string} path 路径
104
+ * @param {object} payload 缓存体
105
+ * @returns {Promise<void>}
106
+ */
107
+ export async function writeOrderCache(path, payload) {
108
+ try {
109
+ await mkdir(dirname(path), { recursive: true })
110
+ await writeFile(path, JSON.stringify(payload), 'utf8')
111
+ }
112
+ catch { /* best-effort cache: dir removed mid-teardown / transient FS error */ }
113
+ }
114
+
115
+ /**
116
+ * @param {string} path 路径
117
+ * @returns {Promise<void>}
118
+ */
119
+ export async function deleteOrderCache(path) {
120
+ try {
121
+ await unlink(path)
122
+ }
123
+ catch { /* absent */ }
124
+ }
@@ -0,0 +1,33 @@
1
+ import { createLruMap } from '../utils/lru.mjs'
2
+
3
+ /**
4
+ * TTL 去重槽:partition bridge、gossip 等共用。
5
+ */
6
+
7
+ /**
8
+ * @param {{ maxSize?: number, ttlMs?: number }} [options] 容量与 TTL
9
+ * @returns {(key: string) => boolean} 首次占用返回 true
10
+ */
11
+ export function createDedupeSlot(options = {}) {
12
+ const maxSize = Number(options.maxSize) || 2000
13
+ const ttlMs = Number(options.ttlMs) || 30_000
14
+ /** @type {Map<string, number>} */
15
+ const seen = createLruMap(maxSize)
16
+
17
+ /**
18
+ * @param {string} key 去重键
19
+ * @returns {boolean} 首次占用返回 true
20
+ */
21
+ const take = key => {
22
+ const now = Date.now()
23
+ const seenAt = seen.get(key)
24
+ if (seenAt !== undefined) {
25
+ if (now - seenAt < ttlMs) return false
26
+ seen.delete(key)
27
+ }
28
+ seen.touch(key, now)
29
+ return true
30
+ }
31
+
32
+ return take
33
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 群级消息限速纯函数(无 I/O,供单测与 governance 复用)。
3
+ */
4
+
5
+ /**
6
+ * @param {object} event DAG 事件
7
+ * @returns {string} 限速实体键
8
+ */
9
+ export function messageRateEntityKey(event) {
10
+ const charId = String(event?.charId || '').trim()
11
+ if (charId) return `char:${charId}`
12
+ return String(event?.sender || '').trim().toLowerCase()
13
+ }
14
+
15
+ /**
16
+ * @param {object} groupSettings 群设置
17
+ * @returns {{ perMin: number, windowMs: number }} 每分钟条数与窗口毫秒
18
+ */
19
+ export function resolveMessageRateLimits(groupSettings) {
20
+ const perMin = Math.max(1, Math.min(120, Number(groupSettings?.messageRateLimitPerMin) || 10))
21
+ const windowMs = Math.max(10_000, Number(groupSettings?.messageRateLimitWindowMs) || 60_000)
22
+ return { perMin, windowMs }
23
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Operator 密钥历史链:entityHash 锚定 recovery 公钥,活跃钥可轮换。
3
+ */
4
+ import { Buffer } from 'node:buffer'
5
+
6
+ import { canonicalStringify } from '../core/canonical_json.mjs'
7
+ import { hashFromPubKeyHex } from '../core/entity_id.mjs'
8
+ import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
9
+
10
+ /**
11
+ *
12
+ */
13
+ export const OPERATOR_KEY_REVOKE_DOMAIN = 'fount-operator-key-revoke'
14
+
15
+ /**
16
+ * @typedef {{
17
+ * generation: number,
18
+ * activePubKeyHex: string,
19
+ * attestedBy: 'recovery' | 'active' | 'revoked',
20
+ * validFrom?: number,
21
+ * revokedGenerations?: number[],
22
+ * }} OperatorKeyHistoryEntry
23
+ */
24
+
25
+ /**
26
+ * @param {unknown} recoveryPubKeyHex 64 位十六进制 recovery 公钥
27
+ * @returns {string} 稳定 subjectHash(entityHash 后半)
28
+ */
29
+ export function recoverySubjectHashFromPubKeyHex(recoveryPubKeyHex) {
30
+ return hashFromPubKeyHex(recoveryPubKeyHex)
31
+ }
32
+
33
+ /**
34
+ * @param {unknown} activePubKeyHex 64 位十六进制 活跃公钥
35
+ * @returns {string} 时间线 sender(pubKeyHash)
36
+ */
37
+ export function activeSenderHashFromPubKeyHex(activePubKeyHex) {
38
+ return hashFromPubKeyHex(activePubKeyHex)
39
+ }
40
+
41
+ /**
42
+ * @param {string} recoveryPubKeyHex recovery 公钥
43
+ * @param {string} activePubKeyHex 初始活跃公钥
44
+ * @param {number} [validFrom] 生效时间
45
+ * @returns {OperatorKeyHistoryEntry[]} 创世链
46
+ */
47
+ export function createGenesisKeyHistory(recoveryPubKeyHex, activePubKeyHex, validFrom = Date.now()) {
48
+ return [{
49
+ generation: 0,
50
+ activePubKeyHex: normalizeHex64(activePubKeyHex),
51
+ attestedBy: 'recovery',
52
+ validFrom,
53
+ }]
54
+ }
55
+
56
+ /**
57
+ * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
58
+ * @param {number} generation 代际
59
+ * @returns {string | null} 活跃公钥 hex
60
+ */
61
+ export function resolveActiveKeyAtGeneration(keyHistory, generation) {
62
+ const entry = keyHistory.find(row => row.generation === generation)
63
+ return entry?.activePubKeyHex ?? null
64
+ }
65
+
66
+ /**
67
+ * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
68
+ * @returns {number} 最新代际,无则 -1
69
+ */
70
+ export function resolveLatestActiveGeneration(keyHistory) {
71
+ if (!keyHistory?.length) return -1
72
+ return Math.max(...keyHistory.map(row => row.generation))
73
+ }
74
+
75
+ /**
76
+ * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
77
+ * @param {number} generation 代际
78
+ * @returns {boolean} 是否已吊销
79
+ */
80
+ export function isActiveGenerationRevoked(keyHistory, generation) {
81
+ for (const entry of keyHistory)
82
+ if (entry.revokedGenerations?.includes(generation)) return true
83
+ return false
84
+ }
85
+
86
+ /**
87
+ * @param {OperatorKeyHistoryEntry[]} keyHistory 密钥历史
88
+ * @param {string} recoveryPubKeyHex recovery 公钥
89
+ * @param {string} senderPubKeyHash 事件 sender(64 hex pubKeyHash)
90
+ * @returns {boolean} sender 是否为未吊销的活跃钥
91
+ */
92
+ export function isValidActiveSender(keyHistory, recoveryPubKeyHex, senderPubKeyHash) {
93
+ const sender = normalizeHex64(senderPubKeyHash)
94
+ if (!isHex64(sender)) return false
95
+ void recoveryPubKeyHex
96
+ for (const entry of keyHistory || []) {
97
+ if (isActiveGenerationRevoked(keyHistory, entry.generation)) continue
98
+ if (activeSenderHashFromPubKeyHex(entry.activePubKeyHex) === sender)
99
+ return true
100
+ }
101
+ return false
102
+ }
103
+
104
+ /**
105
+ * @param {string} recoveryPubKeyHex recovery 公钥
106
+ * @param {string} senderPubKeyHash 事件 sender
107
+ * @returns {boolean} 是否为 recovery 钥签名
108
+ */
109
+ export function isRecoverySender(recoveryPubKeyHex, senderPubKeyHash) {
110
+ return recoverySubjectHashFromPubKeyHex(recoveryPubKeyHex) === normalizeHex64(senderPubKeyHash)
111
+ }
112
+
113
+ /**
114
+ * @param {object} state 物化状态
115
+ * @param {object} event operator_key_rotate 事件
116
+ * @returns {object} 更新后状态
117
+ */
118
+ export function reduceOperatorKeyRotate(state, event) {
119
+ const generation = Number(event.content?.generation)
120
+ const activePubKeyHex = normalizeHex64(event.content?.activePubKeyHex || '')
121
+ if (!Number.isFinite(generation) || generation < 0 || !isHex64(activePubKeyHex))
122
+ return state
123
+ state.operatorKeyHistory = state.operatorKeyHistory || []
124
+ if (state.operatorKeyHistory.some(row => row.generation === generation))
125
+ return state
126
+ state.operatorKeyHistory.push({
127
+ generation,
128
+ activePubKeyHex,
129
+ attestedBy: generation === 0 ? 'recovery' : 'active',
130
+ validFrom: event.hlc?.wall ?? event.timestamp,
131
+ })
132
+ return state
133
+ }
134
+
135
+ /**
136
+ * @param {object} state 物化状态
137
+ * @param {object} event operator_key_revoke 事件
138
+ * @returns {object} 更新后状态
139
+ */
140
+ export function reduceOperatorKeyRevoke(state, event) {
141
+ const newGeneration = Number(event.content?.newGeneration)
142
+ const activePubKeyHex = normalizeHex64(event.content?.activePubKeyHex || '')
143
+ const revokeGenerations = Array.isArray(event.content?.revokeGenerations)
144
+ ? event.content.revokeGenerations.map(g => Number(g)).filter(Number.isFinite)
145
+ : []
146
+ if (!Number.isFinite(newGeneration) || newGeneration < 0 || !isHex64(activePubKeyHex))
147
+ return state
148
+ state.operatorKeyHistory = state.operatorKeyHistory || []
149
+ for (const gen of revokeGenerations) {
150
+ const entry = state.operatorKeyHistory.find(row => row.generation === gen)
151
+ if (entry) {
152
+ entry.revokedGenerations = entry.revokedGenerations || []
153
+ if (!entry.revokedGenerations.includes(gen))
154
+ entry.revokedGenerations.push(gen)
155
+ }
156
+ }
157
+ if (!state.operatorKeyHistory.some(row => row.generation === newGeneration))
158
+ state.operatorKeyHistory.push({
159
+ generation: newGeneration,
160
+ activePubKeyHex,
161
+ attestedBy: 'recovery',
162
+ validFrom: event.hlc?.wall ?? event.timestamp,
163
+ })
164
+
165
+ return state
166
+ }
167
+ /**
168
+ * @param {object[]} events 时间线事件(拓扑序)
169
+ * @returns {{ recoveryPubKeyHex: string | null, operatorKeyHistory: OperatorKeyHistoryEntry[] }} 折叠密钥链
170
+ */
171
+ export function foldOperatorKeyHistoryFromEvents(events) {
172
+ /** @type {OperatorKeyHistoryEntry[]} */
173
+ let operatorKeyHistory = []
174
+ for (const event of events || []) {
175
+ if (event.type === 'operator_key_rotate') {
176
+ const state = reduceOperatorKeyRotate({ operatorKeyHistory }, event)
177
+ operatorKeyHistory = state.operatorKeyHistory
178
+ }
179
+ if (event.type === 'operator_key_revoke') {
180
+ const state = reduceOperatorKeyRevoke({ operatorKeyHistory }, event)
181
+ operatorKeyHistory = state.operatorKeyHistory
182
+ }
183
+ }
184
+ return { recoveryPubKeyHex: null, operatorKeyHistory }
185
+ }
186
+
187
+ /**
188
+ * @param {object} revokeBody 吊销正文
189
+ * @returns {Buffer} 固定域签名消息
190
+ */
191
+ export function operatorKeyRevokeSignBytes(revokeBody) {
192
+ const body = {
193
+ revokeGenerations: (revokeBody.revokeGenerations || []).map(g => Number(g)),
194
+ newGeneration: Number(revokeBody.newGeneration),
195
+ activePubKeyHex: normalizeHex64(revokeBody.activePubKeyHex || ''),
196
+ entityHash: String(revokeBody.entityHash || '').trim().toLowerCase(),
197
+ }
198
+ return Buffer.from(`${OPERATOR_KEY_REVOKE_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
199
+ }
200
+
@@ -0,0 +1,44 @@
1
+ import { eventsToMetas, topologicalCanonicalOrder } from '../dag/index.mjs'
2
+
3
+ /** @type {Map<string, { fp: string, order: string[] }>} */
4
+ const memoByKey = new Map()
5
+
6
+ /**
7
+ * 进程内拓扑序 memo(key 由调用方提供,如 username:groupId)。
8
+ * @param {string} memoKey 缓存键
9
+ * @param {string} fingerprint 文件 stat + 事件数指纹
10
+ * @param {object[]} events 全量事件
11
+ * @param {{ force?: boolean }} [opts] 强制重算
12
+ * @returns {string[]} 拓扑序 event id
13
+ */
14
+ export function resolveTopologicalOrderMemo(memoKey, fingerprint, events, opts = {}) {
15
+ return resolveTopologicalOrderMemoCached(memoKey, fingerprint, () =>
16
+ topologicalCanonicalOrder(eventsToMetas(events)), opts)
17
+ }
18
+
19
+ /**
20
+ * 进程内拓扑序 memo;`resolveOrder` 可接入磁盘缓存等实现。
21
+ * @param {string} memoKey 缓存键
22
+ * @param {string} fingerprint 文件 stat + 事件数指纹
23
+ * @param {() => string[]} resolveOrder 实际求序(含磁盘层)
24
+ * @param {{ force?: boolean }} [opts] 强制重算
25
+ * @returns {string[]} 拓扑序 event id
26
+ */
27
+ export function resolveTopologicalOrderMemoCached(memoKey, fingerprint, resolveOrder, opts = {}) {
28
+ if (!opts.force) {
29
+ const cached = memoByKey.get(memoKey)
30
+ if (cached?.fp === fingerprint && cached.order.length)
31
+ return cached.order
32
+ }
33
+ const order = resolveOrder()
34
+ memoByKey.set(memoKey, { fp: fingerprint, order })
35
+ return order
36
+ }
37
+
38
+ /**
39
+ * @param {string} memoKey 缓存键
40
+ * @returns {void}
41
+ */
42
+ export function invalidateTopologicalOrderMemo(memoKey) {
43
+ memoByKey.delete(memoKey)
44
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * VOLATILE 流:`stream_chunk` 按 chunkSeq 缓冲 slices JSON;终稿由 DAG message_edit 结束(§6.4)。
3
+ *
4
+ * @returns {object} 含 addChunk/end/listChunks/clear 的 API 对象
5
+ */
6
+ export function createVolatileStreamBuffer() {
7
+ /** @type {Map<string, { chunks: Map<number, string>, ended: boolean }>} */
8
+ const streams = new Map()
9
+
10
+ return {
11
+ /**
12
+ * @param {string} pendingStreamId 流 ID
13
+ * @param {number} chunkSeq 分片序号
14
+ * @param {string} payload JSON 序列化的 slices
15
+ * @returns {void}
16
+ */
17
+ addChunk(pendingStreamId, chunkSeq, payload) {
18
+ let stream = streams.get(pendingStreamId)
19
+ if (!stream) {
20
+ stream = { chunks: new Map(), ended: false }
21
+ streams.set(pendingStreamId, stream)
22
+ }
23
+ if (!stream.ended) stream.chunks.set(chunkSeq, payload)
24
+ },
25
+ /**
26
+ * @param {string} pendingStreamId 流 ID
27
+ * @returns {void}
28
+ */
29
+ end(pendingStreamId) {
30
+ const stream = streams.get(pendingStreamId)
31
+ if (stream) stream.ended = true
32
+ },
33
+ /**
34
+ * @param {string} pendingStreamId 流 ID
35
+ * @returns {{ chunkSeq: number, payload: string }[]} 升序分片列表
36
+ */
37
+ listChunks(pendingStreamId) {
38
+ const stream = streams.get(pendingStreamId)
39
+ if (!stream) return []
40
+ return [...stream.chunks.entries()]
41
+ .sort((a, b) => a[0] - b[0])
42
+ .map(([chunkSeq, payload]) => ({ chunkSeq, payload }))
43
+ },
44
+ /**
45
+ * @param {string} pendingStreamId 流 ID
46
+ * @returns {void}
47
+ */
48
+ clear(pendingStreamId) {
49
+ streams.delete(pendingStreamId)
50
+ },
51
+ }
52
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * wantIds 限速与退避(§9):每邻居 QPS/突发、出站批预算、指数退避冷却。
3
+ */
4
+
5
+ const DEFAULT_IN_WINDOW_MS = 60_000
6
+ const DEFAULT_IN_MAX_BATCH = 32
7
+ const DEFAULT_OUT_WINDOW_MS = 60_000
8
+ const DEFAULT_OUT_MAX_BATCH = 16
9
+ const DEFAULT_BACKOFF_BASE_MS = 2_000
10
+ const DEFAULT_BACKOFF_MAX_MS = 120_000
11
+
12
+ /** @type {Map<string, { count: number, resetAt: number }>} */
13
+ const inboundByKey = new Map()
14
+ /** @type {Map<string, { count: number, resetAt: number }>} */
15
+ const outboundByKey = new Map()
16
+ /** @type {Map<string, { until: number, strikes: number }>} */
17
+ const backoffByKey = new Map()
18
+
19
+ /**
20
+ * 清理过期速率窗口条目。
21
+ * @param {Map<string, { count: number, resetAt: number }>} map 速率表
22
+ * @param {number} maxSize 触发清理的上限
23
+ * @param {number} now 当前时间戳
24
+ * @returns {void} 无返回值
25
+ */
26
+ function pruneRateMap(map, maxSize, now) {
27
+ if (map.size <= maxSize) return
28
+ for (const [k, v] of map)
29
+ if (now > v.resetAt + 120_000) map.delete(k)
30
+ }
31
+
32
+ /**
33
+ * 解析 wantIds 限速参数。
34
+ * @param {object} [limits] 可选覆盖
35
+ * @param {number} [limits.inWindowMs] 入站窗口毫秒
36
+ * @param {number} [limits.inMaxBatch] 入站每窗口批次数
37
+ * @param {number} [limits.outWindowMs] 出站窗口毫秒
38
+ * @param {number} [limits.outMaxBatch] 出站每窗口批次数
39
+ * @returns {{ inWindowMs: number, inMaxBatch: number, outWindowMs: number, outMaxBatch: number }} 生效限额
40
+ */
41
+ export function resolveWantIdsLimits(limits = {}) {
42
+ return {
43
+ inWindowMs: Math.max(1000, Number(limits.inWindowMs) || DEFAULT_IN_WINDOW_MS),
44
+ inMaxBatch: Math.max(1, Math.min(256, Number(limits.inMaxBatch) || DEFAULT_IN_MAX_BATCH)),
45
+ outWindowMs: Math.max(1000, Number(limits.outWindowMs) || DEFAULT_OUT_WINDOW_MS),
46
+ outMaxBatch: Math.max(1, Math.min(256, Number(limits.outMaxBatch) || DEFAULT_OUT_MAX_BATCH)),
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {string} groupId 群 ID
52
+ * @param {string} peerId 对端节点 id
53
+ * @returns {string} 复合键
54
+ */
55
+ export function wantIdsPeerKey(groupId, peerId) {
56
+ return `${groupId}\0${peerId}`
57
+ }
58
+
59
+ /**
60
+ * 出站 want 限速键。
61
+ * @param {string} groupId 群 ID
62
+ * @returns {string} 复合键
63
+ */
64
+ export function wantIdsGroupKey(groupId) {
65
+ return groupId
66
+ }
67
+
68
+ /**
69
+ * 是否处于 wantIds 退避冷却。
70
+ * @param {string} key 限速或退避键
71
+ * @returns {boolean} 冷却中则为 true
72
+ */
73
+ export function isWantIdsInBackoff(key) {
74
+ const b = backoffByKey.get(key)
75
+ if (!b) return false
76
+ if (Date.now() < b.until) return true
77
+ backoffByKey.delete(key)
78
+ return false
79
+ }
80
+
81
+ /**
82
+ * 记录一次 wantIds 超限并延长退避。
83
+ * @param {string} key 退避键
84
+ * @returns {void} 无返回值
85
+ */
86
+ export function recordWantIdsBackoff(key) {
87
+ const now = Date.now()
88
+ const prev = backoffByKey.get(key)
89
+ const strikes = (prev?.strikes ?? 0) + 1
90
+ const delay = Math.min(
91
+ DEFAULT_BACKOFF_MAX_MS,
92
+ DEFAULT_BACKOFF_BASE_MS * 2 ** Math.min(strikes - 1, 6),
93
+ )
94
+ backoffByKey.set(key, { until: now + delay, strikes })
95
+ if (backoffByKey.size > 12_000)
96
+ for (const [k, v] of backoffByKey)
97
+ if (now > v.until) backoffByKey.delete(k)
98
+ }
99
+
100
+ /**
101
+ * 消耗入站 want 配额。
102
+ * @param {string} username 用户名
103
+ * @param {string} groupId 群 ID
104
+ * @param {string} requesterId 请求方节点 id
105
+ * @param {object} [limits] 可选限额
106
+ * @returns {boolean} 允许处理则为 true
107
+ */
108
+ export function takeIncomingWantIdsSlot(groupId, requesterId, limits) {
109
+ const { inWindowMs, inMaxBatch } = resolveWantIdsLimits(limits)
110
+ const peerKey = wantIdsPeerKey(groupId, requesterId)
111
+ if (isWantIdsInBackoff(peerKey)) return false
112
+ const now = Date.now()
113
+ let e = inboundByKey.get(peerKey)
114
+ if (!e || now > e.resetAt) e = { count: 0, resetAt: now + inWindowMs }
115
+ if (e.count >= inMaxBatch) {
116
+ recordWantIdsBackoff(peerKey)
117
+ return false
118
+ }
119
+ e.count++
120
+ inboundByKey.set(peerKey, e)
121
+ pruneRateMap(inboundByKey, 8000, now)
122
+ return true
123
+ }
124
+
125
+ /**
126
+ * 消耗出站 want 配额。
127
+ * @param {string} username 用户名
128
+ * @param {string} groupId 群 ID
129
+ * @param {object} [limits] 可选限额
130
+ * @returns {boolean} 允许发起则为 true
131
+ */
132
+ export function takeOutgoingWantIdsSlot(groupId, limits) {
133
+ const { outWindowMs, outMaxBatch } = resolveWantIdsLimits(limits)
134
+ const key = wantIdsGroupKey(groupId)
135
+ if (isWantIdsInBackoff(key)) return false
136
+ const now = Date.now()
137
+ let e = outboundByKey.get(key)
138
+ if (!e || now > e.resetAt) e = { count: 0, resetAt: now + outWindowMs }
139
+ if (e.count >= outMaxBatch) {
140
+ recordWantIdsBackoff(key)
141
+ return false
142
+ }
143
+ e.count++
144
+ outboundByKey.set(key, e)
145
+ pruneRateMap(outboundByKey, 4000, now)
146
+ return true
147
+ }
148
+
149
+ /**
150
+ * 按预算截断 wantIds 列表。
151
+ * @param {string[]} wantIds 缺失事件 id
152
+ * @param {number} budget 单批上限
153
+ * @returns {string[]} 截断后的 id 列表
154
+ */
155
+ export function batchWantIds(wantIds, budget) {
156
+ const cap = Math.max(1, Math.min(256, Number(budget) || DEFAULT_OUT_MAX_BATCH))
157
+ return wantIds.slice(0, cap)
158
+ }