@steve02081504/fount-p2p 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- package/wire/wait.mjs +127 -0
|
@@ -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
|
+
}
|