@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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG / 时间线签名行入库前的共用 hex 规范化(非权限校验)。
|
|
3
|
+
*/
|
|
4
|
+
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
5
|
+
import { assertHex64, HEX_ID_64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
6
|
+
/**
|
|
7
|
+
* @param {Record<string, unknown>} obj 可变对象
|
|
8
|
+
* @param {string} key 字段名
|
|
9
|
+
*/
|
|
10
|
+
function canonicalizeHexField(obj, key) {
|
|
11
|
+
if (obj[key] == null || obj[key] === '') return
|
|
12
|
+
const normalized = normalizeHex64(obj[key])
|
|
13
|
+
if (!HEX_ID_64.test(normalized))
|
|
14
|
+
throw new Error(`${key} must be 64 hex characters`)
|
|
15
|
+
obj[key] = normalized
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {unknown} content 事件 content
|
|
20
|
+
* @param {ReadonlySet<string>} hexKeys content 内 hex64 字段名
|
|
21
|
+
* @param {ReadonlySet<string>} [entityHashKeys] content 内 128 位 entityHash 字段名
|
|
22
|
+
* @returns {object | undefined} 规范化后的 content
|
|
23
|
+
*/
|
|
24
|
+
export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Set()) {
|
|
25
|
+
if (!content) return content
|
|
26
|
+
const out = { ...content }
|
|
27
|
+
for (const key of hexKeys)
|
|
28
|
+
canonicalizeHexField(out, key)
|
|
29
|
+
for (const key of entityHashKeys) {
|
|
30
|
+
if (out[key] == null || out[key] === '') continue
|
|
31
|
+
const entityHash = String(out[key]).toLowerCase()
|
|
32
|
+
if (!isEntityHash128(entityHash))
|
|
33
|
+
throw new Error(`${key} must be 128 hex characters`)
|
|
34
|
+
out[key] = entityHash
|
|
35
|
+
}
|
|
36
|
+
if (out.content_ref) {
|
|
37
|
+
const ref = { ...out.content_ref }
|
|
38
|
+
canonicalizeHexField(ref, 'contentHash')
|
|
39
|
+
out.content_ref = ref
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} event 签名事件
|
|
46
|
+
* @param {{
|
|
47
|
+
* prepare?: (event: object) => object,
|
|
48
|
+
* contentHexKeys?: ReadonlySet<string>,
|
|
49
|
+
* entityHashKeys?: ReadonlySet<string>,
|
|
50
|
+
* }} [opts] 各域字段集
|
|
51
|
+
* @returns {object} canonical 行
|
|
52
|
+
*/
|
|
53
|
+
export function canonicalizeSignedRow(event, opts = {}) {
|
|
54
|
+
const out = opts.prepare ? opts.prepare({ ...event }) : { ...event }
|
|
55
|
+
out.id = assertHex64(out.id, 'id')
|
|
56
|
+
out.sender = assertHex64(out.sender, 'sender')
|
|
57
|
+
if (Array.isArray(out.prev_event_ids))
|
|
58
|
+
out.prev_event_ids = out.prev_event_ids.map((id, index) =>
|
|
59
|
+
assertHex64(id, `prev_event_ids[${index}]`),
|
|
60
|
+
)
|
|
61
|
+
const hexKeys = opts.contentHexKeys
|
|
62
|
+
if (out.content && hexKeys?.size)
|
|
63
|
+
out.content = canonicalizeRowContent(out.content, hexKeys, opts.entityHashKeys)
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG 事件列表查询辅助。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object[]} events 事件列表
|
|
7
|
+
* @param {string} type 事件类型
|
|
8
|
+
* @returns {{ event: object, index: number } | null} 自后向前最近一条
|
|
9
|
+
*/
|
|
10
|
+
export function findLastEventOfType(events, type) {
|
|
11
|
+
for (let index = events.length - 1; index >= 0; index--)
|
|
12
|
+
if (events[index]?.type === type) return { event: events[index], index }
|
|
13
|
+
|
|
14
|
+
return null
|
|
15
|
+
}
|
package/dag/index.mjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
6
|
+
import { HEX_ID_64 } from '../core/hexIds.mjs'
|
|
7
|
+
import { HLC } from '../core/hlc.mjs'
|
|
8
|
+
import { sign, verify } from '../crypto/crypto.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 重导出 64 位十六进制事件 ID 正则(`HEX_ID_64` 别名)。
|
|
12
|
+
*/
|
|
13
|
+
export { HEX_ID_64 as EVENT_ID_HEX }
|
|
14
|
+
const EVENT_ID_HEX = HEX_ID_64
|
|
15
|
+
const HEX_PAIR = /^[\da-f]{2}$/iu
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 对事件 id 列表做二叉 Merkle 根(字典序叶子,§7 checkpoint)。
|
|
19
|
+
* @param {string[]} ids 事件 id
|
|
20
|
+
* @returns {string} 64 位十六进制根
|
|
21
|
+
*/
|
|
22
|
+
export function merkleRoot(ids) {
|
|
23
|
+
const sorted = [...new Set(ids.filter(id => EVENT_ID_HEX.test(String(id))))].sort()
|
|
24
|
+
if (!sorted.length)
|
|
25
|
+
return createHash('sha256').update('', 'utf8').digest('hex')
|
|
26
|
+
/** @type {Buffer[]} */
|
|
27
|
+
let level = sorted.map(id => createHash('sha256').update(id, 'utf8').digest())
|
|
28
|
+
while (level.length > 1) {
|
|
29
|
+
/** @type {Buffer[]} */
|
|
30
|
+
const next = []
|
|
31
|
+
for (let index = 0; index < level.length; index += 2) {
|
|
32
|
+
const left = level[index]
|
|
33
|
+
const right = index + 1 < level.length ? level[index + 1] : left
|
|
34
|
+
next.push(createHash('sha256').update(Buffer.concat([left, right])).digest())
|
|
35
|
+
}
|
|
36
|
+
level = next
|
|
37
|
+
}
|
|
38
|
+
return Buffer.from(level[0]).toString('hex')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 本节点可见 DAG 叶集合摘要(§7.1 `local_tips_hash`)。
|
|
43
|
+
* @param {string[]} tipIds 叶事件 id
|
|
44
|
+
* @returns {string} SHA-256 十六进制
|
|
45
|
+
*/
|
|
46
|
+
export function computeLocalTipsHash(tipIds) {
|
|
47
|
+
const sorted = [...new Set(tipIds.filter(id => EVENT_ID_HEX.test(String(id))))].sort()
|
|
48
|
+
return createHash('sha256').update(sorted.join(','), 'utf8').digest('hex')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 规范化父 id 列表:去重、去空、字典序(§6 验签域)。
|
|
53
|
+
* @param {unknown} raw 原始 prev_event_ids 字段
|
|
54
|
+
* @returns {string[]} 去重并字典序排序后的父事件 id 数组
|
|
55
|
+
*/
|
|
56
|
+
export function sortedPrevEventIds(raw) {
|
|
57
|
+
if (!Array.isArray(raw)) return []
|
|
58
|
+
return [...new Set(raw.filter(id => EVENT_ID_HEX.test(String(id))))].sort()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 移除对象中值为 `undefined` 的键(浅拷贝)。
|
|
63
|
+
* @param {object} object 输入对象
|
|
64
|
+
* @returns {object} 不含 undefined 值的浅拷贝
|
|
65
|
+
*/
|
|
66
|
+
function stripUndefined(object) {
|
|
67
|
+
/** @type {Record<string, unknown>} */
|
|
68
|
+
const out = {}
|
|
69
|
+
for (const [key, value] of Object.entries(object))
|
|
70
|
+
if (value !== undefined) out[key] = value
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 从事件中取出参与 ID 计算与签名的字段(不含 id、signature、received_at 等)。
|
|
76
|
+
* @param {object} event 完整事件对象
|
|
77
|
+
* @returns {object} 用于 canonical 序列化与验签的正文子集
|
|
78
|
+
*/
|
|
79
|
+
export function eventBodyForSign(event) {
|
|
80
|
+
const body = {
|
|
81
|
+
type: event.type,
|
|
82
|
+
groupId: event.groupId,
|
|
83
|
+
channelId: event.channelId ?? null,
|
|
84
|
+
sender: event.sender,
|
|
85
|
+
charId: event.charId ?? null,
|
|
86
|
+
timestamp: event.timestamp,
|
|
87
|
+
hlc: event.hlc,
|
|
88
|
+
prev_event_ids: sortedPrevEventIds(event.prev_event_ids),
|
|
89
|
+
content: event.content,
|
|
90
|
+
}
|
|
91
|
+
if (event.node_id != null) body.node_id = event.node_id
|
|
92
|
+
return stripUndefined(body)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 事件 ID = SHA256(canonical unsigned body) 十六进制
|
|
97
|
+
* @param {object} body `eventBodyForSign` 或同形对象
|
|
98
|
+
* @returns {string} SHA256 十六进制事件 id
|
|
99
|
+
*/
|
|
100
|
+
export function computeEventId(body) {
|
|
101
|
+
const normalized = { ...body }
|
|
102
|
+
if ('prev_event_ids' in normalized)
|
|
103
|
+
normalized.prev_event_ids = sortedPrevEventIds(normalized.prev_event_ids)
|
|
104
|
+
return createHash('sha256').update(canonicalStringify(stripUndefined(normalized)), 'utf8').digest('hex')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 验签 / 哈希用的 UTF-8 字节序列
|
|
109
|
+
* @param {object} body 与 `computeEventId` 同形的正文对象
|
|
110
|
+
* @returns {import('node:buffer').Buffer} canonical JSON 的 UTF-8 字节
|
|
111
|
+
*/
|
|
112
|
+
export function signPayloadBytes(body) {
|
|
113
|
+
return Buffer.from(canonicalStringify(stripUndefined({
|
|
114
|
+
...body,
|
|
115
|
+
prev_event_ids: sortedPrevEventIds(body.prev_event_ids),
|
|
116
|
+
})), 'utf8')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 最小堆(按 compare 取最小元素)。
|
|
121
|
+
* @template T
|
|
122
|
+
*/
|
|
123
|
+
class MinHeap {
|
|
124
|
+
/** @type {T[]} */
|
|
125
|
+
#data = []
|
|
126
|
+
/** @type {(left: T, right: T) => number} */
|
|
127
|
+
#compare
|
|
128
|
+
|
|
129
|
+
/** @param {(left: T, right: T) => number} compare 比较函数 */
|
|
130
|
+
constructor(compare) {
|
|
131
|
+
this.#compare = compare
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @returns {number} 元素个数 */
|
|
135
|
+
get size() {
|
|
136
|
+
return this.#data.length
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {T} value 入堆 */
|
|
140
|
+
push(value) {
|
|
141
|
+
const data = this.#data
|
|
142
|
+
data.push(value)
|
|
143
|
+
let index = data.length - 1
|
|
144
|
+
while (index > 0) {
|
|
145
|
+
const parent = (index - 1) >> 1
|
|
146
|
+
if (this.#compare(data[index], data[parent]) >= 0) break
|
|
147
|
+
;[data[index], data[parent]] = [data[parent], data[index]]
|
|
148
|
+
index = parent
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @returns {T | undefined} 弹出最小元素 */
|
|
153
|
+
pop() {
|
|
154
|
+
const data = this.#data
|
|
155
|
+
if (!data.length) return undefined
|
|
156
|
+
const top = data[0]
|
|
157
|
+
const last = data.pop()
|
|
158
|
+
if (data.length && last !== undefined) {
|
|
159
|
+
data[0] = last
|
|
160
|
+
let index = 0
|
|
161
|
+
for (; ;) {
|
|
162
|
+
const left = index * 2 + 1
|
|
163
|
+
const right = left + 1
|
|
164
|
+
let smallest = index
|
|
165
|
+
if (left < data.length && this.#compare(data[left], data[smallest]) < 0)
|
|
166
|
+
smallest = left
|
|
167
|
+
if (right < data.length && this.#compare(data[right], data[smallest]) < 0)
|
|
168
|
+
smallest = right
|
|
169
|
+
if (smallest === index) break
|
|
170
|
+
;[data[index], data[smallest]] = [data[smallest], data[index]]
|
|
171
|
+
index = smallest
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return top
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 规范拓扑序(Kahn + tiebreaker:hlc.wall, hlc.logical, node_id, id;与分布式群聊规范 §4 一致)。
|
|
180
|
+
* @param {Array<{ id: string, prev_event_ids?: unknown, hlc?: { wall: number, logical: number }, sender?: string, node_id?: string }>} metas 事件元数据列表
|
|
181
|
+
* @returns {string[]} 按规范顺序排列的事件 id 列表
|
|
182
|
+
*/
|
|
183
|
+
export function topologicalCanonicalOrder(metas) {
|
|
184
|
+
if (!metas.length) return []
|
|
185
|
+
const byId = new Map(metas.map(meta => [meta.id, meta]))
|
|
186
|
+
const parentCount = new Map()
|
|
187
|
+
/** @type {Map<string, string[]>} */
|
|
188
|
+
const children = new Map()
|
|
189
|
+
for (const meta of metas) {
|
|
190
|
+
const parentsInGraph = sortedPrevEventIds(meta.prev_event_ids).filter(parentId => byId.has(parentId))
|
|
191
|
+
parentCount.set(meta.id, parentsInGraph.length)
|
|
192
|
+
for (const parentId of parentsInGraph) {
|
|
193
|
+
const list = children.get(parentId)
|
|
194
|
+
if (list) list.push(meta.id)
|
|
195
|
+
else children.set(parentId, [meta.id])
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} eventId 事件 id
|
|
201
|
+
* @returns {{ wall: number, logical: number }} wall 与 logical 分量
|
|
202
|
+
*/
|
|
203
|
+
const hlcOf = eventId => {
|
|
204
|
+
const hlc = byId.get(eventId)?.hlc
|
|
205
|
+
return { wall: Number(hlc?.wall) || 0, logical: Number(hlc?.logical) || 0 }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} leftId 事件 id
|
|
210
|
+
* @param {string} rightId 事件 id
|
|
211
|
+
* @returns {number} 比较结果
|
|
212
|
+
*/
|
|
213
|
+
const compareIds = (leftId, rightId) => {
|
|
214
|
+
const leftHlc = hlcOf(leftId)
|
|
215
|
+
const rightHlc = hlcOf(rightId)
|
|
216
|
+
if (leftHlc.wall !== rightHlc.wall) return leftHlc.wall - rightHlc.wall
|
|
217
|
+
if (leftHlc.logical !== rightHlc.logical) return leftHlc.logical - rightHlc.logical
|
|
218
|
+
const leftNode = String(byId.get(leftId)?.node_id || '')
|
|
219
|
+
const rightNode = String(byId.get(rightId)?.node_id || '')
|
|
220
|
+
if (leftNode !== rightNode) return leftNode.localeCompare(rightNode)
|
|
221
|
+
return leftId.localeCompare(rightId)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ordered = []
|
|
225
|
+
const ready = new MinHeap(compareIds)
|
|
226
|
+
for (const meta of metas)
|
|
227
|
+
if (parentCount.get(meta.id) === 0) ready.push(meta.id)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
while (ready.size) {
|
|
231
|
+
const next = ready.pop()
|
|
232
|
+
if (next == null) break
|
|
233
|
+
ordered.push(next)
|
|
234
|
+
for (const childId of children.get(next) || []) {
|
|
235
|
+
const remaining = (parentCount.get(childId) || 0) - 1
|
|
236
|
+
parentCount.set(childId, remaining)
|
|
237
|
+
if (remaining === 0) ready.push(childId)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return ordered
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} hex 十六进制串
|
|
246
|
+
* @param {number} expectedByteLength 期望字节长度
|
|
247
|
+
* @returns {Uint8Array | null} 解析后的字节
|
|
248
|
+
*/
|
|
249
|
+
function parseHexBytes(hex, expectedByteLength) {
|
|
250
|
+
if (!hex || hex.length !== expectedByteLength * 2 || hex.length % 2 !== 0) return null
|
|
251
|
+
const bytes = hex.match(/.{2}/g)
|
|
252
|
+
if (!bytes?.every(pair => HEX_PAIR.test(pair))) return null
|
|
253
|
+
return new Uint8Array(bytes.map(byte => Number.parseInt(byte, 16)))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 签名事件(签名为不含 id 的 canonical body)
|
|
258
|
+
* @param {object} event 含完整字段;可含占位 id
|
|
259
|
+
* @param {Uint8Array} privateKey 私钥
|
|
260
|
+
* @returns {Promise<string>} 十六进制签名
|
|
261
|
+
*/
|
|
262
|
+
export async function signEvent(event, privateKey) {
|
|
263
|
+
const body = eventBodyForSign(event)
|
|
264
|
+
const signature = await sign(signPayloadBytes(body), privateKey)
|
|
265
|
+
return Array.from(signature).map(byte => byte.toString(16).padStart(2, '0')).join('')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 验证事件签名
|
|
270
|
+
* @param {object} event 含 signature、sender 与正文字段的完整事件
|
|
271
|
+
* @returns {Promise<boolean>} 验签是否通过
|
|
272
|
+
*/
|
|
273
|
+
export async function verifyEventSignature(event) {
|
|
274
|
+
try {
|
|
275
|
+
const signature = parseHexBytes(event?.signature, 64)
|
|
276
|
+
const publicKey = parseHexBytes(event?.sender, 32)
|
|
277
|
+
if (!signature || !publicKey) return false
|
|
278
|
+
return await verify(signature, signPayloadBytes(eventBodyForSign(event)), publicKey)
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.error('Signature verification failed:', error)
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 创建新事件
|
|
288
|
+
* @param {object} params 事件字段与签名私钥等构造参数
|
|
289
|
+
* @param {string[]} [params.prev_event_ids] 父事件 id;根事件 `[]`
|
|
290
|
+
* @returns {Promise<object>} 含 id、signature 的完整事件对象
|
|
291
|
+
*/
|
|
292
|
+
export async function createEvent(params) {
|
|
293
|
+
const {
|
|
294
|
+
type, groupId, channelId, sender, charId, content, prev_event_ids, privateKey, hlc,
|
|
295
|
+
} = params
|
|
296
|
+
|
|
297
|
+
const event = {
|
|
298
|
+
type,
|
|
299
|
+
groupId,
|
|
300
|
+
channelId: channelId || null,
|
|
301
|
+
sender,
|
|
302
|
+
charId: charId || null,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
hlc: hlc || HLC.now(),
|
|
305
|
+
prev_event_ids: sortedPrevEventIds(prev_event_ids),
|
|
306
|
+
content,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
event.id = computeEventId(eventBodyForSign(event))
|
|
310
|
+
event.signature = await signEvent(event, privateKey)
|
|
311
|
+
return event
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {object[]} events DAG 事件行
|
|
316
|
+
* @returns {Array<{ id: string, prev_event_ids?: unknown, hlc?: object, node_id?: string, sender?: string }>} 拓扑 meta 列表
|
|
317
|
+
*/
|
|
318
|
+
export function eventsToMetas(events) {
|
|
319
|
+
return events.map(event => ({
|
|
320
|
+
id: event.id,
|
|
321
|
+
prev_event_ids: event.prev_event_ids,
|
|
322
|
+
hlc: event.hlc,
|
|
323
|
+
node_id: event.node_id,
|
|
324
|
+
sender: event.sender,
|
|
325
|
+
}))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
*
|
|
330
|
+
*/
|
|
331
|
+
export { canonicalizeRowContent, canonicalizeSignedRow } from './canonicalize_row.mjs'
|
|
332
|
+
/**
|
|
333
|
+
*
|
|
334
|
+
*/
|
|
335
|
+
export { stripDagEventLocalExtensions } from './strip_extensions.mjs'
|