@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
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @steve02081504/fount-p2p
|
|
2
|
+
|
|
3
|
+
fount federation P2P layer: node identity, link handshake, TrustGraph, Mailbox store-and-forward, DAG timeline, EVFS entity files.
|
|
4
|
+
|
|
5
|
+
## Used by
|
|
6
|
+
|
|
7
|
+
- [fount](https://github.com/steve02081504/fount)
|
|
8
|
+
- [subfount](https://github.com/steve02081504/subfount)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @steve02081504/fount-p2p
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires **Node.js ≥ 20** (ESM + `import ... with { type: 'json' }`).
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { startNode, createScopedLinkRoom, ensureUserRoom } from '@steve02081504/fount-p2p'
|
|
22
|
+
|
|
23
|
+
await startNode({ nodeDir: '/path/to/p2p/node' })
|
|
24
|
+
await ensureUserRoom()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Subpath exports mirror source directories, e.g. `@steve02081504/fount-p2p/dag`, `@steve02081504/fount-p2p/transport/link_registry`, `@steve02081504/fount-p2p/registries/event_type`.
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
| Layer | Directory | Role |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| L0 | `core/` | Pure primitives: `hexIds`, `entity_id*`, `canonical_json` |
|
|
34
|
+
| L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
|
|
35
|
+
| L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
|
|
36
|
+
| L3 | `discovery/`, `link/`, `transport/`, `rooms/` | Discovery, RTC links, rooms |
|
|
37
|
+
| L4 | `trust_graph/`, `mailbox/`, `dag/`, `federation/`, `files/`, `entity/` | Federation, store-and-forward, DAG, EVFS |
|
|
38
|
+
| — | `registries/` | Pluggable registries (event type, part path, room provider, …) |
|
|
39
|
+
|
|
40
|
+
Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscoveryProvider`, …).
|
|
41
|
+
|
|
42
|
+
Root contains only the facade and package metadata; all modules live in layered subdirectories. After a layout migration, run `node scripts/cleanup-root-duplicates.mjs` to drop stale root stubs.
|
|
43
|
+
|
|
44
|
+
## Tests
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm test # pure + integration (--test-force-exit: node-datachannel leaves native handles)
|
|
48
|
+
npm run test:live # RTC / link smoke (requires node-datachannel)
|
|
49
|
+
npm run test:sim # tunables co-evolution sim (dev only, not published; --social-tunables to write back)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
During development: `node scripts/check-imports.mjs` validates relative imports. After a layout migration, `node scripts/cleanup-root-duplicates.mjs` removes stale root-level stubs.
|
|
53
|
+
|
|
54
|
+
## Optional dependencies
|
|
55
|
+
|
|
56
|
+
- `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth discovery (attempted by default; skipped when not installed)
|
|
57
|
+
|
|
58
|
+
Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
|
|
59
|
+
|
|
60
|
+
## Docs
|
|
61
|
+
|
|
62
|
+
- Signaling and link setup: [`docs/signaling.md`](./docs/signaling.md)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 / hex / bytes 互转(无 Node 依赖,浏览器与 Node 共用)。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {ArrayBuffer} buffer 原始二进制缓冲
|
|
7
|
+
* @returns {string} 标准 Base64 文本
|
|
8
|
+
*/
|
|
9
|
+
export function arrayBufferToBase64(buffer) {
|
|
10
|
+
let binary = ''
|
|
11
|
+
const bytes = new Uint8Array(buffer)
|
|
12
|
+
for (let index = 0; index < bytes.byteLength; index++)
|
|
13
|
+
binary += String.fromCharCode(bytes[index])
|
|
14
|
+
return btoa(binary)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Uint8Array} u8 原始字节
|
|
19
|
+
* @returns {string} 标准 Base64 文本
|
|
20
|
+
*/
|
|
21
|
+
export function u8ToB64(u8) {
|
|
22
|
+
let binary = ''
|
|
23
|
+
for (let index = 0; index < u8.length; index++) binary += String.fromCharCode(u8[index])
|
|
24
|
+
return btoa(binary)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} b64 标准 Base64 文本
|
|
29
|
+
* @returns {Uint8Array} 解码后的字节
|
|
30
|
+
*/
|
|
31
|
+
export function b64ToU8(b64) {
|
|
32
|
+
const bin = atob(b64)
|
|
33
|
+
const out = new Uint8Array(bin.length)
|
|
34
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} hex 十六进制文本(可带 0x 前缀,长度须为 2×nBytes)
|
|
40
|
+
* @param {number} nBytes 目标字节数
|
|
41
|
+
* @returns {Uint8Array} 解析得到的字节数组
|
|
42
|
+
*/
|
|
43
|
+
export function hexToNBytes(hex, nBytes) {
|
|
44
|
+
const normalizedHex = String(hex).replace(/^0x/iu, '').trim()
|
|
45
|
+
if (normalizedHex.length !== nBytes * 2 || !/^[\da-f]+$/iu.test(normalizedHex))
|
|
46
|
+
throw new RangeError('invalid hex length or characters')
|
|
47
|
+
const out = new Uint8Array(nBytes)
|
|
48
|
+
for (let index = 0; index < nBytes; index++)
|
|
49
|
+
out[index] = parseInt(normalizedHex.slice(index * 2, index * 2 + 2), 16)
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 确定性 JSON 序列化(键排序),用于 event id 与验签负载
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} value 任意可 JSON 化的结构(禁止 BigInt)
|
|
5
|
+
* @returns {string} 键已排序的紧凑 JSON 文本
|
|
6
|
+
*/
|
|
7
|
+
export function canonicalStringify(value) {
|
|
8
|
+
if (value === null || value === undefined) return JSON.stringify(value)
|
|
9
|
+
if (typeof value === 'bigint') throw new TypeError('BigInt not allowed in canonical JSON')
|
|
10
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string')
|
|
11
|
+
return JSON.stringify(value)
|
|
12
|
+
if (Array.isArray(value))
|
|
13
|
+
return `[${value.map(item => item === undefined ? 'null' : canonicalStringify(item)).join(',')}]`
|
|
14
|
+
if (typeof value === 'object') {
|
|
15
|
+
// 跳过值为 undefined 的键,与 JSON.stringify 语义一致:否则 canonical 串会写出字面量 `undefined`,
|
|
16
|
+
// 而事件经 JSON wire/落盘往返后这些键会消失,导致 computeEventId 两端不一致(id_mismatch)。
|
|
17
|
+
const keys = Object.keys(value).filter(k => value[k] !== undefined).sort()
|
|
18
|
+
return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalStringify(value[k])}`).join(',')}}`
|
|
19
|
+
}
|
|
20
|
+
throw new TypeError(`Unsupported type: ${typeof value}`)
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** 进程内 Map 复合键(`\0` 分隔;V8 对短 ConsString 拼接很快)。 */
|
|
2
|
+
|
|
3
|
+
const SEP = '\0'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {...string} parts 键段(至少一段)
|
|
7
|
+
* @returns {string} 复合键
|
|
8
|
+
*/
|
|
9
|
+
export function compositeKey(...parts) {
|
|
10
|
+
if (!parts.length) throw new Error('compositeKey: at least one part required')
|
|
11
|
+
return parts.join(SEP)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {...string} prefixParts 前缀段(可为空,表示整表)
|
|
16
|
+
* @returns {string} 带尾部分隔符的前缀(用于子树匹配)
|
|
17
|
+
*/
|
|
18
|
+
function compositePrefix(...prefixParts) {
|
|
19
|
+
return prefixParts.length ? `${compositeKey(...prefixParts)}${SEP}` : ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @template V
|
|
24
|
+
* @param {Map<string, V>} map 根表
|
|
25
|
+
* @param {...string} parts 键段
|
|
26
|
+
* @returns {V | undefined} 命中值
|
|
27
|
+
*/
|
|
28
|
+
export function mapGet(map, ...parts) {
|
|
29
|
+
return map.get(compositeKey(...parts))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template V
|
|
34
|
+
* @param {Map<string, V>} map 根表
|
|
35
|
+
* @param {...string} partsAndValue 键段 + 末尾值
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
export function mapSet(map, ...partsAndValue) {
|
|
39
|
+
const value = partsAndValue.pop()
|
|
40
|
+
map.set(compositeKey(...partsAndValue), value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @template V
|
|
45
|
+
* @param {Map<string, V>} map 根表
|
|
46
|
+
* @param {...string} parts 键段
|
|
47
|
+
* @returns {boolean} 是否存在
|
|
48
|
+
*/
|
|
49
|
+
export function mapHas(map, ...parts) {
|
|
50
|
+
return map.has(compositeKey(...parts))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @template V
|
|
55
|
+
* @param {Map<string, V>} map 根表
|
|
56
|
+
* @param {...string} parts 键段
|
|
57
|
+
* @returns {void}
|
|
58
|
+
*/
|
|
59
|
+
export function mapDelete(map, ...parts) {
|
|
60
|
+
map.delete(compositeKey(...parts))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 删除以 `prefixParts` 为前缀的所有条目(含更深层级)。
|
|
65
|
+
* @template V
|
|
66
|
+
* @param {Map<string, V>} map 根表
|
|
67
|
+
* @param {...string} prefixParts 前缀段(空则清空整表)
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
70
|
+
export function mapDeleteByPrefix(map, ...prefixParts) {
|
|
71
|
+
const prefix = compositePrefix(...prefixParts)
|
|
72
|
+
for (const k of [...map.keys()])
|
|
73
|
+
if (!prefix || k.startsWith(prefix)) map.delete(k)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 遍历前缀子树;回调收到前缀之后的各段与值。
|
|
78
|
+
* @template V
|
|
79
|
+
* @param {Map<string, V>} map 根表
|
|
80
|
+
* @param {...(string | ((tail: string[], value: V) => void))} prefixPartsAndFn 前缀段 + 末尾回调
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
export function mapForEachUnder(map, ...prefixPartsAndFn) {
|
|
84
|
+
const fn = prefixPartsAndFn.pop()
|
|
85
|
+
const prefix = compositePrefix(...prefixPartsAndFn)
|
|
86
|
+
for (const [k, value] of map) {
|
|
87
|
+
if (!k.startsWith(prefix)) continue
|
|
88
|
+
const tail = k.slice(prefix.length).split(SEP).filter(Boolean)
|
|
89
|
+
fn(tail, value)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
|
|
2
|
+
import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
|
|
3
|
+
|
|
4
|
+
/** @type {readonly string[]} 权限键注册表顺序(运算期 BigInt 编码用) */
|
|
5
|
+
export const PERMISSION_REGISTRY_ORDER = Object.freeze([
|
|
6
|
+
'VIEW_CHANNEL',
|
|
7
|
+
'SEND_MESSAGES',
|
|
8
|
+
'SEND_STICKERS',
|
|
9
|
+
'ADD_REACTIONS',
|
|
10
|
+
'MANAGE_MESSAGES',
|
|
11
|
+
'MANAGE_CHANNELS',
|
|
12
|
+
'KICK_MEMBERS',
|
|
13
|
+
'BAN_MEMBERS',
|
|
14
|
+
'MANAGE_ROLES',
|
|
15
|
+
'MANAGE_ADMINS',
|
|
16
|
+
'INVITE_MEMBERS',
|
|
17
|
+
'STREAM',
|
|
18
|
+
'CREATE_THREADS',
|
|
19
|
+
'UPLOAD_FILES',
|
|
20
|
+
'MANAGE_FILES',
|
|
21
|
+
'PIN_MESSAGES',
|
|
22
|
+
'SET_WORLD',
|
|
23
|
+
'ADMIN',
|
|
24
|
+
'BYPASS_RATE_LIMIT',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 默认晚消息冻结时间(毫秒)
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_LATE_MESSAGE_FREEZE_MS = 30_000
|
|
31
|
+
/**
|
|
32
|
+
* 占位 `message` 无 `message_edit` 终稿时的空闲截断(毫秒);§6.4 `streamGeneratingIdleMs` 默认。
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_STREAM_GENERATING_IDLE_MS = 150_000
|
|
35
|
+
/**
|
|
36
|
+
* 默认最大捕获事件数
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_MAX_CATCHUP_EVENTS = 50_000
|
|
39
|
+
/**
|
|
40
|
+
* 成员页面大小
|
|
41
|
+
*/
|
|
42
|
+
export const MEMBERS_PAGE_SIZE = 500
|
|
43
|
+
/** Checkpoint 中保留的 epoch 链历史条数上限 */
|
|
44
|
+
export const EPOCH_CHAIN_MAX = 256
|
|
45
|
+
|
|
46
|
+
/** 群文件经联邦复制的单块上限(字节,§10.2) */
|
|
47
|
+
export const FEDERATION_CHUNK_MAX_BYTES = trustGraphTunables.federationChunkMaxBytes
|
|
48
|
+
|
|
49
|
+
/** TrustGraph fanout 参考 Top-K(N=8 roster 时的缩放值;运行时请用 resolveFederationFanoutTopK) */
|
|
50
|
+
export const FEDERATION_FANOUT_TOP_K = resolveFederationFanoutTopK(8, trustGraphTunables)
|
|
51
|
+
|
|
52
|
+
/** 全局 fed_chunk_get miss 时 fanout 邻居数 */
|
|
53
|
+
export const FEDERATION_CHUNK_FETCH_FANOUT_K = trustGraphTunables.federationChunkFetchFanoutK
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import { pubKeyHash } from '../crypto/crypto.mjs'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
encodeEntityHash,
|
|
7
|
+
ENTITY_HASH_RE,
|
|
8
|
+
isEntityHash128,
|
|
9
|
+
parseEntityHash,
|
|
10
|
+
} from './entity_id_parse.mjs'
|
|
11
|
+
import { isHex64, normalizeHex64 } from './hexIds.mjs'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
export {
|
|
17
|
+
encodeEntityHash,
|
|
18
|
+
ENTITY_HASH_RE,
|
|
19
|
+
isEntityHash128,
|
|
20
|
+
parseEntityHash,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {unknown} pubKeyHex 32 字节公钥 hex
|
|
25
|
+
* @returns {string} 64 位 nodeHash / subjectHash(pubKeyHash)
|
|
26
|
+
*/
|
|
27
|
+
export function hashFromPubKeyHex(pubKeyHex) {
|
|
28
|
+
const hex = normalizeHex64(pubKeyHex)
|
|
29
|
+
if (!isHex64(hex) || Buffer.from(hex, 'hex').length !== 32)
|
|
30
|
+
throw new Error('invalid pubKeyHex')
|
|
31
|
+
return pubKeyHash(Buffer.from(hex, 'hex'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} nodeHash 成员所属节点 hash
|
|
36
|
+
* @param {string} recoveryPubKeyHex 32 字节 recovery 公钥 hex(稳定身份锚)
|
|
37
|
+
* @returns {string} 用户 entityHash
|
|
38
|
+
*/
|
|
39
|
+
export function userEntityHashFromRecoveryPubKeyHex(nodeHash, recoveryPubKeyHex) {
|
|
40
|
+
return encodeEntityHash(nodeHash, hashFromPubKeyHex(recoveryPubKeyHex))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} nodeHash 成员所属节点 hash
|
|
45
|
+
* @param {string} subjectHash 成员签名 pubKeyHash(DAG sender)
|
|
46
|
+
* @returns {string} 用户 entityHash
|
|
47
|
+
*/
|
|
48
|
+
export function userEntityHashFromSubjectHash(nodeHash, subjectHash) {
|
|
49
|
+
const node = normalizeHex64(nodeHash)
|
|
50
|
+
const subject = normalizeHex64(subjectHash)
|
|
51
|
+
if (!isHex64(node) || !isHex64(subject)) throw new Error('invalid subject hash')
|
|
52
|
+
return encodeEntityHash(node, subject)
|
|
53
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { isHex64, normalizeHex64 } from './hexIds.mjs'
|
|
2
|
+
|
|
3
|
+
/** 128 位小写 hex:`nodeHash(64)` + `subjectHash(64)`。 */
|
|
4
|
+
export const ENTITY_HASH_RE = /^[\da-f]{128}$/u
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value 待校验值
|
|
8
|
+
* @returns {boolean} 是否为合法 128 位 hex
|
|
9
|
+
*/
|
|
10
|
+
export function isEntityHash128(value) {
|
|
11
|
+
const raw = String(value ?? '').trim().toLowerCase().replace(/^0x/iu, '')
|
|
12
|
+
return ENTITY_HASH_RE.test(raw)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} entityHash 128 位 entityHash
|
|
17
|
+
* @returns {{ entityHash: string, nodeHash: string, subjectHash: string } | null} 解析结果;非法时 null
|
|
18
|
+
*/
|
|
19
|
+
export function parseEntityHash(entityHash) {
|
|
20
|
+
const raw = String(entityHash ?? '').trim().toLowerCase().replace(/^0x/iu, '')
|
|
21
|
+
if (!ENTITY_HASH_RE.test(raw)) return null
|
|
22
|
+
return {
|
|
23
|
+
entityHash: raw,
|
|
24
|
+
nodeHash: raw.slice(0, 64),
|
|
25
|
+
subjectHash: raw.slice(64, 128),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} nodeHash 所属节点(64 hex)
|
|
31
|
+
* @param {string} subjectHash 主体 hash(64 hex)
|
|
32
|
+
* @returns {string} 128 位 entityHash
|
|
33
|
+
*/
|
|
34
|
+
export function encodeEntityHash(nodeHash, subjectHash) {
|
|
35
|
+
const node = normalizeHex64(nodeHash)
|
|
36
|
+
const subject = normalizeHex64(subjectHash)
|
|
37
|
+
if (!isHex64(node) || !isHex64(subject))
|
|
38
|
+
throw new Error('invalid entity hash parts')
|
|
39
|
+
return node + subject
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} logicalPath EVFS 逻辑路径
|
|
3
|
+
* @returns {string} 规范化后的相对路径
|
|
4
|
+
*/
|
|
5
|
+
export function assertSafeEvfsLogicalPath(logicalPath) {
|
|
6
|
+
const raw = String(logicalPath || '').trim()
|
|
7
|
+
if (!raw || raw.includes('\0'))
|
|
8
|
+
throw new Error('invalid EVFS path')
|
|
9
|
+
const segments = raw.split(/[/\\]+/).map(s => s.trim()).filter(Boolean)
|
|
10
|
+
if (!segments.length) throw new Error('invalid EVFS path')
|
|
11
|
+
for (const segment of segments)
|
|
12
|
+
if (segment === '.' || segment === '..')
|
|
13
|
+
throw new Error('invalid EVFS path traversal')
|
|
14
|
+
return segments.join('/')
|
|
15
|
+
}
|
package/core/hexIds.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** 64 位小写十六进制(DAG 事件 id、公钥哈希等)。 */
|
|
2
|
+
export const HEX_ID_64 = /^[\da-f]{64}$/u
|
|
3
|
+
|
|
4
|
+
/** 签名 hex(128 字符)。 */
|
|
5
|
+
export const SIGNATURE_HEX_128 = /^[\da-f]{128}$/u
|
|
6
|
+
|
|
7
|
+
/** `blob:<64hex>` 存储定位符。 */
|
|
8
|
+
export const BLOB_STORAGE_LOCATOR_RE = /^blob:([\da-f]{64})$/u
|
|
9
|
+
|
|
10
|
+
/** `local:…/chunks/<64hex>.bin` 群分块路径。 */
|
|
11
|
+
export const LOCAL_CHUNK_FILE_RE = /^local:[^/]+\/chunks\/([\da-f]{64})\.bin$/u
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {unknown} value 原始字符串
|
|
15
|
+
* @returns {string} trim + 去 0x + 小写
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeHex64(value) {
|
|
18
|
+
return String(value ?? '').trim().toLowerCase().replace(/^0x/iu, '')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {unknown} value 待校验值
|
|
23
|
+
* @returns {boolean} 是否为 64 位 hex
|
|
24
|
+
*/
|
|
25
|
+
export function isHex64(value) {
|
|
26
|
+
return HEX_ID_64.test(normalizeHex64(value))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 64 位 hex eventId 字典序比较(固定宽度 ASCII,不用 localeCompare)。
|
|
31
|
+
* @param {unknown} a 左操作数
|
|
32
|
+
* @param {unknown} b 右操作数
|
|
33
|
+
* @returns {number} 排序比较结果
|
|
34
|
+
*/
|
|
35
|
+
export function compareHex64Asc(a, b) {
|
|
36
|
+
const sa = normalizeHex64(a)
|
|
37
|
+
const sb = normalizeHex64(b)
|
|
38
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 外部入站专用:规范化并断言 64 位 hex。
|
|
43
|
+
* @param {unknown} value 原始值
|
|
44
|
+
* @param {string} [label='hex64'] 字段名(错误信息)
|
|
45
|
+
* @returns {string} 小写 64 位 hex
|
|
46
|
+
*/
|
|
47
|
+
export function assertHex64(value, label = 'hex64') {
|
|
48
|
+
const normalized = normalizeHex64(value)
|
|
49
|
+
if (!HEX_ID_64.test(normalized))
|
|
50
|
+
throw new Error(`${label} must be 64 hex characters`)
|
|
51
|
+
return normalized
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} value 待校验值
|
|
56
|
+
* @returns {boolean} 是否为 128 位签名 hex
|
|
57
|
+
*/
|
|
58
|
+
export function isSignatureHex128(value) {
|
|
59
|
+
return SIGNATURE_HEX_128.test(String(value ?? '').trim())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 外部入站专用:断言 128 位签名 hex。
|
|
64
|
+
* @param {unknown} value 原始值
|
|
65
|
+
* @param {string} [label='signature'] 字段名
|
|
66
|
+
* @returns {string} 签名 hex
|
|
67
|
+
*/
|
|
68
|
+
export function assertSignatureHex128(value, label = 'signature') {
|
|
69
|
+
const normalized = String(value ?? '').trim().toLowerCase()
|
|
70
|
+
if (!SIGNATURE_HEX_128.test(normalized))
|
|
71
|
+
throw new Error(`${label} must be 128 hex characters`)
|
|
72
|
+
return normalized
|
|
73
|
+
}
|
package/core/hlc.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 混合逻辑时钟 (Hybrid Logical Clock)
|
|
3
|
+
* 用于分布式系统中的事件排序
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 混合逻辑时钟实例:由物理 wall 与 logical 计数组成。
|
|
8
|
+
*/
|
|
9
|
+
export class HLC {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} [wall=0] - 物理时间戳(毫秒)
|
|
12
|
+
* @param {number} [logical=0] - 逻辑计数(同 wall 下递增)
|
|
13
|
+
*/
|
|
14
|
+
constructor(wall = 0, logical = 0) {
|
|
15
|
+
this.wall = wall
|
|
16
|
+
this.logical = logical
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 创建当前时间的 HLC
|
|
21
|
+
* @returns {HLC} wall 为当前时间、logical 为 0 的实例
|
|
22
|
+
*/
|
|
23
|
+
static now() {
|
|
24
|
+
return new HLC(Date.now(), 0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 更新 HLC(接收到远程事件时)
|
|
29
|
+
* @param {HLC} remote - 远程 HLC
|
|
30
|
+
* @returns {HLC} 合并规则后的新 HLC 实例
|
|
31
|
+
*/
|
|
32
|
+
update(remote) {
|
|
33
|
+
const localWall = Date.now()
|
|
34
|
+
|
|
35
|
+
if (localWall > this.wall && localWall > remote.wall)
|
|
36
|
+
return new HLC(localWall, 0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if (this.wall === remote.wall)
|
|
40
|
+
return new HLC(this.wall, Math.max(this.logical, remote.logical) + 1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if (this.wall > remote.wall)
|
|
44
|
+
return new HLC(this.wall, this.logical + 1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
return new HLC(remote.wall, remote.logical + 1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 递增 HLC(本地生成新事件时)
|
|
52
|
+
* @returns {HLC} tick 后的新实例
|
|
53
|
+
*/
|
|
54
|
+
tick() {
|
|
55
|
+
const localWall = Date.now()
|
|
56
|
+
|
|
57
|
+
if (localWall > this.wall)
|
|
58
|
+
return new HLC(localWall, 0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
return new HLC(this.wall, this.logical + 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 比较两个 HLC
|
|
66
|
+
* @param {HLC} other - 另一个 HLC
|
|
67
|
+
* @returns {number} 小于零、零或大于零(与 `this - other` 同号)
|
|
68
|
+
*/
|
|
69
|
+
compare(other) {
|
|
70
|
+
if (this.wall !== other.wall)
|
|
71
|
+
return this.wall - other.wall
|
|
72
|
+
|
|
73
|
+
return this.logical - other.logical
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 转换为 JSON
|
|
78
|
+
* @returns {{ wall: number, logical: number }} 可序列化的纯对象
|
|
79
|
+
*/
|
|
80
|
+
toJSON() {
|
|
81
|
+
return {
|
|
82
|
+
wall: this.wall,
|
|
83
|
+
logical: this.logical
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 从 JSON 创建 HLC
|
|
89
|
+
* @param {{ wall: number, logical: number }} json - JSON 对象
|
|
90
|
+
* @returns {HLC} 恢复的 HLC 实例
|
|
91
|
+
*/
|
|
92
|
+
static fromJSON(json) {
|
|
93
|
+
return new HLC(json.wall, json.logical)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 生成新事件的 HLC:取上一个事件的 HLC 与当前墙上时间的合并结果(`update`),
|
|
99
|
+
* 保证新 HLC 严格晚于 lastHlc(Lamport 单调性)且不早于 `wallMs`。
|
|
100
|
+
*
|
|
101
|
+
* @param {{ wall: number, logical: number } | null | undefined} lastHlcJson 上一事件的 HLC(来自已持久化事件;缺省时从当前时间起)
|
|
102
|
+
* @param {number} [wallMs] 当前事件的期望物理时间戳(ms);缺省为 `Date.now()`
|
|
103
|
+
* @returns {{ wall: number, logical: number }} 新事件 HLC 的可序列化纯对象
|
|
104
|
+
*/
|
|
105
|
+
export function nextHlc(lastHlcJson, wallMs) {
|
|
106
|
+
const wall = typeof wallMs === 'number' && Number.isFinite(wallMs)
|
|
107
|
+
? Math.max(wallMs, Date.now())
|
|
108
|
+
: Date.now()
|
|
109
|
+
const remote = new HLC(wall, 0)
|
|
110
|
+
if (!lastHlcJson || typeof lastHlcJson.wall !== 'number')
|
|
111
|
+
return remote.toJSON()
|
|
112
|
+
const last = HLC.fromJSON(lastHlcJson)
|
|
113
|
+
return last.update(remote).toJSON()
|
|
114
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-key 信封:X25519 ECIES 包装与 AES-GCM 消息载荷(wire scheme 由消费方定义,如 ckg)。
|
|
3
|
+
* 解密 payload 不可脱离外层 DAG Ed25519 签名上下文单独传递或信任。
|
|
4
|
+
*/
|
|
5
|
+
import { Buffer } from 'node:buffer'
|
|
6
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
import { unwrapKeyEcies, wrapKeyEcies } from './key.mjs'
|
|
9
|
+
|
|
10
|
+
/** @type {'ckg'} 频道消息 content 加密 scheme */
|
|
11
|
+
export const CKG_SCHEME = 'ckg'
|
|
12
|
+
|
|
13
|
+
/** @typedef {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} EciesWrapBlob */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 生成随机 32 字节频道密钥(hex)。
|
|
17
|
+
* @returns {string} 32 字节 hex 频道密钥
|
|
18
|
+
*/
|
|
19
|
+
export function generateChannelKey() {
|
|
20
|
+
return randomBytes(32).toString('hex')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* X25519 ECIES 包装 K_ch 给成员 Ed25519 公钥。
|
|
25
|
+
* @param {string} channelKeyHex 32 字节 hex
|
|
26
|
+
* @param {string} memberEdPubKeyHex 64 位十六进制
|
|
27
|
+
* @returns {EciesWrapBlob} ECIES 包装结果
|
|
28
|
+
*/
|
|
29
|
+
export function wrapChannelKey(channelKeyHex, memberEdPubKeyHex) {
|
|
30
|
+
return wrapKeyEcies(channelKeyHex, memberEdPubKeyHex)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {EciesWrapBlob} wrap ECIES 密文
|
|
35
|
+
* @param {Uint8Array} myEdPrivKeySeed 32 字节 Ed25519 种子
|
|
36
|
+
* @returns {string | null} K_ch hex
|
|
37
|
+
*/
|
|
38
|
+
export function unwrapChannelKey(wrap, myEdPrivKeySeed) {
|
|
39
|
+
return unwrapKeyEcies(wrap, myEdPrivKeySeed)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} channelKeyHex K_ch
|
|
44
|
+
* @param {string} channelId 频道 id(AAD 盐)
|
|
45
|
+
* @param {number} generation 代际
|
|
46
|
+
* @returns {Buffer} 消息 AES-256 密钥
|
|
47
|
+
*/
|
|
48
|
+
function messageAesKey(channelKeyHex, channelId, generation) {
|
|
49
|
+
return Buffer.from(hkdfSync(
|
|
50
|
+
'sha256',
|
|
51
|
+
Buffer.from(channelKeyHex, 'hex'),
|
|
52
|
+
`ckg:${String(channelId)}:${String(generation)}`,
|
|
53
|
+
'',
|
|
54
|
+
32,
|
|
55
|
+
))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} plaintext UTF-8 / JSON 字符串
|
|
60
|
+
* @param {string} channelKeyHex K_ch
|
|
61
|
+
* @param {string} channelId 频道 ID
|
|
62
|
+
* @param {number} generation 密钥代际
|
|
63
|
+
* @returns {{ scheme: typeof CKG_SCHEME, channelId: string, generation: number, payload: string }} 频道密钥信封
|
|
64
|
+
*/
|
|
65
|
+
export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, generation) {
|
|
66
|
+
const key = messageAesKey(channelKeyHex, channelId, generation)
|
|
67
|
+
const iv = randomBytes(12)
|
|
68
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
69
|
+
const plain = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, 'utf8')
|
|
70
|
+
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
|
|
71
|
+
const authTag = cipher.getAuthTag()
|
|
72
|
+
return {
|
|
73
|
+
scheme: CKG_SCHEME,
|
|
74
|
+
channelId: String(channelId),
|
|
75
|
+
generation: Number(generation) || 0,
|
|
76
|
+
payload: `${iv.toString('base64')}.${ciphertext.toString('base64')}.${authTag.toString('base64')}`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {{ scheme?: string, channelId?: string, generation?: number, payload: string }} envelope 频道密钥信封
|
|
82
|
+
* @param {string} channelKeyHex K_ch
|
|
83
|
+
* @param {string} channelId 频道 ID
|
|
84
|
+
* @returns {string | null} 明文 UTF-8
|
|
85
|
+
*/
|
|
86
|
+
export function decryptWithChannelKey(envelope, channelKeyHex, channelId) {
|
|
87
|
+
if (envelope?.scheme !== CKG_SCHEME || !envelope.payload) return null
|
|
88
|
+
try {
|
|
89
|
+
const parts = envelope.payload.split('.')
|
|
90
|
+
if (parts.length !== 3) return null
|
|
91
|
+
const generation = Number(envelope.generation) || 0
|
|
92
|
+
const key = messageAesKey(channelKeyHex, channelId, generation)
|
|
93
|
+
const iv = Buffer.from(parts[0], 'base64')
|
|
94
|
+
const ciphertext = Buffer.from(parts[1], 'base64')
|
|
95
|
+
const authTag = Buffer.from(parts[2], 'base64')
|
|
96
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv)
|
|
97
|
+
decipher.setAuthTag(authTag)
|
|
98
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
|
|
99
|
+
}
|
|
100
|
+
catch { return null }
|
|
101
|
+
}
|