@steve02081504/fount-p2p 0.0.26 → 0.0.28
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 +3 -2
- package/core/bytes_codec.mjs +5 -6
- package/core/composite_key.mjs +0 -79
- package/core/entity_id_parse.mjs +2 -3
- package/core/evfs_logical_path.mjs +2 -3
- package/core/hexIds.mjs +5 -5
- package/core/logical_entity.mjs +3 -4
- package/crypto/channel.mjs +2 -25
- package/crypto/checkpoint_sign.mjs +5 -4
- package/crypto/crypto.mjs +0 -8
- package/crypto/key.mjs +9 -10
- package/dag/canonicalize_row.mjs +1 -1
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +7 -12
- package/discovery/bt/index.mjs +6 -11
- package/discovery/bt/peer_hints.mjs +2 -3
- package/discovery/bt/runtime.mjs +1 -1
- package/discovery/index.mjs +14 -14
- package/discovery/lan.mjs +3 -3
- package/discovery/lan_interfaces.mjs +2 -2
- package/discovery/lan_peer_hints.mjs +1 -1
- package/discovery/nostr.mjs +19 -24
- package/federation/entity_key_chain.mjs +5 -24
- package/federation/message_rate_limit.mjs +3 -2
- package/federation/part_query/runtime.mjs +9 -20
- package/files/assemble.mjs +3 -3
- package/files/assemble_stream.mjs +1 -1
- package/files/chunk/fetch.mjs +1 -2
- package/files/chunk/responder.mjs +4 -41
- package/files/chunk/store.mjs +2 -3
- package/files/chunk/verify.mjs +2 -3
- package/files/evfs.mjs +1 -1
- package/files/evfs_ref.mjs +3 -4
- package/files/fetch_fanout.mjs +3 -3
- package/files/manifest/fetch.mjs +2 -3
- package/files/manifest/normalize.mjs +9 -9
- package/files/manifest/pending.mjs +1 -4
- package/files/manifest/public.mjs +8 -8
- package/files/transfer_key.mjs +3 -3
- package/files/transfer_key_registry.mjs +6 -6
- package/governance/branch.mjs +2 -1
- package/governance/join_pow.mjs +7 -5
- package/governance/owner_succession_ballot.mjs +6 -6
- package/index.mjs +8 -2
- package/infra/default_node_dir.mjs +2 -2
- package/infra/priority.mjs +2 -2
- package/link/frame.mjs +2 -3
- package/link/handshake.mjs +6 -6
- package/link/pipe.mjs +69 -5
- package/link/providers/ble_gatt.mjs +1 -9
- package/link/providers/index.mjs +2 -2
- package/link/providers/nostr.mjs +1 -3
- package/link/providers/webrtc.mjs +2 -2
- package/link/rtc/ice_local_hostname.mjs +6 -5
- package/link/rtc/w3c_bridge.mjs +1 -1
- package/link/sdp_fingerprint.mjs +1 -1
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +4 -4
- package/mailbox/parse.mjs +1 -1
- package/mailbox/prune.mjs +2 -2
- package/mailbox/rate.mjs +3 -10
- package/mailbox/store.mjs +7 -7
- package/node/denylist.mjs +38 -38
- package/node/entity_store.mjs +5 -5
- package/node/identity.mjs +7 -8
- package/node/instance.mjs +1 -1
- package/node/log.mjs +1 -1
- package/node/network.mjs +14 -16
- package/node/personal_block.mjs +22 -20
- package/node/reputation_store.mjs +11 -19
- package/node/reputation_sync.mjs +0 -7
- package/node/signaling_config.mjs +1 -1
- package/node/storage_plugins.mjs +2 -2
- package/overlay/index.mjs +1 -1
- package/package.json +1 -1
- package/registries/event_type.mjs +2 -2
- package/registries/room_provider.mjs +2 -2
- package/reputation/engine.mjs +17 -15
- package/schemas/discovery.mjs +5 -5
- package/schemas/federation_pull.mjs +16 -24
- package/schemas/part_query.mjs +4 -4
- package/timeline/verify_remote.mjs +4 -4
- package/transport/group_link_set.mjs +3 -3
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +51 -20
- package/transport/mesh_keepalive.mjs +1 -1
- package/transport/node_scope/wire.mjs +1 -1
- package/transport/peer_health.mjs +127 -0
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +6 -6
- package/transport/runtime_bootstrap.mjs +12 -12
- package/trust_graph/cache.mjs +2 -3
- package/trust_graph/engine.mjs +1 -1
- package/trust_graph/registry.mjs +2 -2
- package/trust_graph/send.mjs +1 -1
- package/utils/async_mutex.mjs +1 -1
- package/wire/ingress.mjs +1 -1
- package/wire/part/ingress.mjs +6 -6
- package/wire/part/query.mjs +3 -3
- package/wire/volatile_signature.mjs +4 -9
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Reputation pull/apply is separate: `pullReputationFromNode` → JSON; `setReputa
|
|
|
69
69
|
## Layout
|
|
70
70
|
|
|
71
71
|
| Layer | Directory | Role |
|
|
72
|
-
|
|
72
|
+
| --- | --- | --- |
|
|
73
73
|
| L0 | `core/` | Pure primitives: `hexIds`, `entity_id*`, `canonical_json` |
|
|
74
74
|
| L1 | `crypto/`, `wire/`, `schemas/` | Cryptography, wire protocol, canonical validation |
|
|
75
75
|
| L2 | `node/` | Node runtime: `identity`, `entity_store`, `denylist`, `reputation_store` |
|
|
@@ -83,7 +83,7 @@ Facade entry: `index.mjs` (`startNode`, `createGroupLinkSet`, `registerDiscovery
|
|
|
83
83
|
**Public transport modules:**
|
|
84
84
|
|
|
85
85
|
| Module | Role |
|
|
86
|
-
|
|
86
|
+
| --- | --- |
|
|
87
87
|
| `link_registry.mjs` | fount-network facade: dial fallback, scope/overlay |
|
|
88
88
|
| `user_room.mjs` / `group_link_set.mjs` / `node_scope/wire` + `node_scope/features` | rooms + composable node-scope wires (`registerNodeScopeWireHook`, attaches) |
|
|
89
89
|
| `room_scopes.mjs` / `remote_user_room.mjs` | scope constants / remote user slot |
|
|
@@ -123,3 +123,4 @@ Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStora
|
|
|
123
123
|
- Infra relay / node-scope attaches: [`docs/infra.md`](./docs/infra.md)
|
|
124
124
|
- Wire part / fanout attaches: [`docs/wire.md`](./docs/wire.md)
|
|
125
125
|
- EVFS public manifests / fetch: [`docs/evfs.md`](./docs/evfs.md)
|
|
126
|
+
- Subjective reputation: [`docs/reputation.md`](./docs/reputation.md)
|
package/core/bytes_codec.mjs
CHANGED
|
@@ -29,16 +29,15 @@ export function bytesToHex(bytes) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* @param {string} hex hex
|
|
32
|
+
* @param {string} hex hex 文本(偶数字符;大小写均可)
|
|
33
33
|
* @returns {Uint8Array} 解码后的字节
|
|
34
34
|
*/
|
|
35
35
|
export function hexToBytes(hex) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const out = new Uint8Array(text.length / 2)
|
|
36
|
+
if (hex.length % 2) throw new Error('p2p: hex length must be even')
|
|
37
|
+
const out = new Uint8Array(hex.length / 2)
|
|
39
38
|
for (let index = 0; index < out.length; index++) {
|
|
40
|
-
const high = hexNibble(
|
|
41
|
-
const low = hexNibble(
|
|
39
|
+
const high = hexNibble(hex.charCodeAt(index * 2))
|
|
40
|
+
const low = hexNibble(hex.charCodeAt(index * 2 + 1))
|
|
42
41
|
if (high < 0 || low < 0) throw new Error('p2p: invalid hex')
|
|
43
42
|
out[index] = (high << 4) | low
|
|
44
43
|
}
|
package/core/composite_key.mjs
CHANGED
|
@@ -10,82 +10,3 @@ export function compositeKey(...parts) {
|
|
|
10
10
|
if (!parts.length) throw new Error('compositeKey: at least one part required')
|
|
11
11
|
return parts.join(SEP)
|
|
12
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))} prefixPartsAndCallback 前缀段 + 末尾回调
|
|
81
|
-
* @returns {void}
|
|
82
|
-
*/
|
|
83
|
-
export function mapForEachUnder(map, ...prefixPartsAndCallback) {
|
|
84
|
-
const callback = prefixPartsAndCallback.pop()
|
|
85
|
-
const prefix = compositePrefix(...prefixPartsAndCallback)
|
|
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
|
-
callback(tail, value)
|
|
90
|
-
}
|
|
91
|
-
}
|
package/core/entity_id_parse.mjs
CHANGED
|
@@ -8,8 +8,7 @@ export const ENTITY_HASH_RE = /^[\da-f]{128}$/u
|
|
|
8
8
|
* @returns {boolean} 是否为合法 128 位 hex
|
|
9
9
|
*/
|
|
10
10
|
export function isEntityHash128(value) {
|
|
11
|
-
|
|
12
|
-
return ENTITY_HASH_RE.test(raw)
|
|
11
|
+
return ENTITY_HASH_RE.test(String(value ?? '').replace(/^0x/iu, ''))
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -17,7 +16,7 @@ export function isEntityHash128(value) {
|
|
|
17
16
|
* @returns {{ entityHash: string, nodeHash: string, subjectHash: string } | null} 解析结果;非法时 null
|
|
18
17
|
*/
|
|
19
18
|
export function parseEntityHash(entityHash) {
|
|
20
|
-
const raw = String(entityHash ?? '').
|
|
19
|
+
const raw = String(entityHash ?? '').replace(/^0x/iu, '')
|
|
21
20
|
if (!ENTITY_HASH_RE.test(raw)) return null
|
|
22
21
|
return {
|
|
23
22
|
entityHash: raw,
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* @returns {string} 规范化后的相对路径
|
|
4
4
|
*/
|
|
5
5
|
export function assertSafeEvfsLogicalPath(logicalPath) {
|
|
6
|
-
|
|
7
|
-
if (!raw || raw.includes('\0'))
|
|
6
|
+
if (!logicalPath || logicalPath.includes('\0'))
|
|
8
7
|
throw new Error('invalid EVFS path')
|
|
9
|
-
const segments =
|
|
8
|
+
const segments = logicalPath.split(/[/\\]+/).filter(Boolean)
|
|
10
9
|
if (!segments.length) throw new Error('invalid EVFS path')
|
|
11
10
|
for (const segment of segments)
|
|
12
11
|
if (segment === '.' || segment === '..')
|
package/core/hexIds.mjs
CHANGED
|
@@ -12,15 +12,15 @@ export const LOCAL_CHUNK_FILE_RE = /^local:[^/]+\/chunks\/([\da-f]{64})\.bin$/u
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @param {unknown} value 原始字符串
|
|
15
|
-
* @returns {string}
|
|
15
|
+
* @returns {string} 去可选 0x 前缀后的字符串(不修大小写/空白)
|
|
16
16
|
*/
|
|
17
17
|
export function normalizeHex64(value) {
|
|
18
|
-
return String(value ?? '').
|
|
18
|
+
return String(value ?? '').replace(/^0x/iu, '')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* @param {unknown} value 待校验值
|
|
23
|
-
* @returns {boolean} 是否为 64
|
|
23
|
+
* @returns {boolean} 是否为 64 位小写 hex
|
|
24
24
|
*/
|
|
25
25
|
export function isHex64(value) {
|
|
26
26
|
return HEX_ID_64.test(normalizeHex64(value))
|
|
@@ -39,7 +39,7 @@ export function compareHex64Asc(a, b) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* 外部入站专用:规范化并断言 64
|
|
42
|
+
* 外部入站专用:规范化并断言 64 位小写 hex。
|
|
43
43
|
* @param {unknown} value 原始值
|
|
44
44
|
* @param {string} [label='hex64'] 字段名(错误信息)
|
|
45
45
|
* @returns {string} 小写 64 位 hex
|
|
@@ -56,5 +56,5 @@ export function assertHex64(value, label = 'hex64') {
|
|
|
56
56
|
* @returns {boolean} 是否为 128 位签名 hex
|
|
57
57
|
*/
|
|
58
58
|
export function isSignatureHex128(value) {
|
|
59
|
-
return SIGNATURE_HEX_128.test(String(value ?? '')
|
|
59
|
+
return SIGNATURE_HEX_128.test(String(value ?? ''))
|
|
60
60
|
}
|
package/core/logical_entity.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256Hex } from '../crypto/crypto.mjs'
|
|
2
2
|
|
|
3
3
|
import { encodeEntityHash, parseEntityHash } from './entity_id_parse.mjs'
|
|
4
4
|
|
|
@@ -10,9 +10,8 @@ export const LOGICAL_ENTITY_SENTINEL_NODE_HASH = '0'.repeat(64)
|
|
|
10
10
|
* @returns {string} 128 位 logical entityHash
|
|
11
11
|
*/
|
|
12
12
|
export function logicalEntityHash(subject) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return encodeEntityHash(LOGICAL_ENTITY_SENTINEL_NODE_HASH, sha256TextHex(s))
|
|
13
|
+
if (!subject) throw new Error('subject required')
|
|
14
|
+
return encodeEntityHash(LOGICAL_ENTITY_SENTINEL_NODE_HASH, sha256Hex(subject))
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/**
|
package/crypto/channel.mjs
CHANGED
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
import { Buffer } from 'node:buffer'
|
|
6
6
|
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto'
|
|
7
7
|
|
|
8
|
-
import { unwrapKeyEcies, wrapKeyEcies } from './key.mjs'
|
|
9
|
-
|
|
10
8
|
/** @type {'channel-key'} 频道消息 content 加密 scheme */
|
|
11
9
|
export const CHANNEL_KEY_SCHEME = 'channel-key'
|
|
12
10
|
|
|
13
|
-
/** @typedef {{ ephemPub: string, iv: string, ciphertext: string, authTag: string }} EciesWrapBlob */
|
|
14
|
-
|
|
15
11
|
/**
|
|
16
12
|
* 生成随机 32 字节频道密钥(hex)。
|
|
17
13
|
* @returns {string} 32 字节 hex 频道密钥
|
|
@@ -20,25 +16,6 @@ export function generateChannelKey() {
|
|
|
20
16
|
return randomBytes(32).toString('hex')
|
|
21
17
|
}
|
|
22
18
|
|
|
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
19
|
/**
|
|
43
20
|
* @param {string} channelKeyHex K_ch
|
|
44
21
|
* @param {string} channelId 频道 id(AAD 盐)
|
|
@@ -49,7 +26,7 @@ function messageAesKey(channelKeyHex, channelId, generation) {
|
|
|
49
26
|
return Buffer.from(hkdfSync(
|
|
50
27
|
'sha256',
|
|
51
28
|
Buffer.from(channelKeyHex, 'hex'),
|
|
52
|
-
`${CHANNEL_KEY_SCHEME}:${
|
|
29
|
+
`${CHANNEL_KEY_SCHEME}:${channelId}:${generation}`,
|
|
53
30
|
'',
|
|
54
31
|
32,
|
|
55
32
|
))
|
|
@@ -71,7 +48,7 @@ export function encryptWithChannelKey(plaintext, channelKeyHex, channelId, gener
|
|
|
71
48
|
const authTag = cipher.getAuthTag()
|
|
72
49
|
return {
|
|
73
50
|
scheme: CHANNEL_KEY_SCHEME,
|
|
74
|
-
channelId
|
|
51
|
+
channelId,
|
|
75
52
|
generation: Number(generation) || 0,
|
|
76
53
|
payload: `${iv.toString('base64')}.${ciphertext.toString('base64')}.${authTag.toString('base64')}`,
|
|
77
54
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
3
|
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
4
|
+
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
4
5
|
import { merkleRoot } from '../dag/index.mjs'
|
|
5
6
|
|
|
6
7
|
import { sign, verify } from './crypto.mjs'
|
|
@@ -26,8 +27,8 @@ export async function signCheckpoint(payload, secretKey) {
|
|
|
26
27
|
* @returns {Promise<boolean>} 合法为 true
|
|
27
28
|
*/
|
|
28
29
|
export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
|
|
29
|
-
const raw = checkpoint.checkpoint_signature
|
|
30
|
-
if (
|
|
30
|
+
const raw = String(checkpoint.checkpoint_signature || '')
|
|
31
|
+
if (!isSignatureHex128(raw)) return false
|
|
31
32
|
const body = { ...checkpoint }
|
|
32
33
|
delete body.checkpoint_signature
|
|
33
34
|
const messageBytes = Buffer.from(canonicalStringify(body), 'utf8')
|
|
@@ -40,7 +41,7 @@ export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
|
|
|
40
41
|
* @returns {boolean} 签名格式合法为 true
|
|
41
42
|
*/
|
|
42
43
|
export function isSignedCheckpoint(checkpoint) {
|
|
43
|
-
return
|
|
44
|
+
return isSignatureHex128(String(checkpoint?.checkpoint_signature || ''))
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -59,7 +60,7 @@ export async function verifyRemoteCheckpoint(checkpoint) {
|
|
|
59
60
|
const ownerHash = checkpoint.members_record?.delegatedOwnerPubKeyHash
|
|
60
61
|
const owner = checkpoint.members_record?.members?.[ownerHash]
|
|
61
62
|
const pubHex = owner?.pubKeyHex
|
|
62
|
-
if (!pubHex ||
|
|
63
|
+
if (!pubHex || !isHex64(pubHex))
|
|
63
64
|
return { valid: false, reason: 'delegated owner pubkey missing' }
|
|
64
65
|
if (!await verifyCheckpointSignature(checkpoint, Buffer.from(pubHex, 'hex')))
|
|
65
66
|
return { valid: false, reason: 'checkpoint signature invalid' }
|
package/crypto/crypto.mjs
CHANGED
|
@@ -81,11 +81,3 @@ export function pubKeyHash(publicKey) {
|
|
|
81
81
|
export function sha256Hex(data) {
|
|
82
82
|
return bytesToHex(sha256(inputBytes(data)))
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @param {string} text 文本
|
|
87
|
-
* @returns {string} 64 字符 hex
|
|
88
|
-
*/
|
|
89
|
-
export function sha256TextHex(text) {
|
|
90
|
-
return sha256Hex(String(text ?? ''))
|
|
91
|
-
}
|
package/crypto/key.mjs
CHANGED
|
@@ -73,7 +73,7 @@ function toHBuf(H) {
|
|
|
73
73
|
* @returns {Buffer} 32 字节 AES-256 密钥
|
|
74
74
|
*/
|
|
75
75
|
export function deriveFileKey(H, fileId) {
|
|
76
|
-
return kdf(toHBuf(H), 'file',
|
|
76
|
+
return kdf(toHBuf(H), 'file', fileId)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
@@ -83,7 +83,7 @@ export function deriveFileKey(H, fileId) {
|
|
|
83
83
|
* @returns {Buffer} 32 字节 AES-256 密钥
|
|
84
84
|
*/
|
|
85
85
|
export function deriveSocialPostKey(H, postId) {
|
|
86
|
-
return kdf(toHBuf(H), 'post',
|
|
86
|
+
return kdf(toHBuf(H), 'post', postId)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
@@ -93,7 +93,7 @@ export function deriveSocialPostKey(H, postId) {
|
|
|
93
93
|
* @returns {Buffer} 32 字节 HMAC 密钥
|
|
94
94
|
*/
|
|
95
95
|
export function deriveStreamingAuthKey(H, groupId) {
|
|
96
|
-
return kdf(toHBuf(H), 'streaming',
|
|
96
|
+
return kdf(toHBuf(H), 'streaming', groupId)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// ─── H 轮换(§11.2、§6.3 key_rotate)──────────────────────────────────────
|
|
@@ -109,8 +109,8 @@ export function deriveStreamingAuthKey(H, groupId) {
|
|
|
109
109
|
export function deriveNextFileMasterKey(oldKeyHex, eventId, nonce) {
|
|
110
110
|
return createHash('sha256')
|
|
111
111
|
.update(Buffer.from(oldKeyHex, 'hex'))
|
|
112
|
-
.update(
|
|
113
|
-
.update(
|
|
112
|
+
.update(eventId)
|
|
113
|
+
.update(nonce)
|
|
114
114
|
.digest('hex')
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -245,7 +245,7 @@ export function encryptUtf8ForMember(utf8Text, memberEdPubKeyHex) {
|
|
|
245
245
|
const wrapKey = createHash('sha256').update(sharedSecret).digest()
|
|
246
246
|
const iv = randomBytes(12)
|
|
247
247
|
const cipher = createCipheriv('aes-256-gcm', wrapKey, iv)
|
|
248
|
-
const plain = Buffer.from(
|
|
248
|
+
const plain = Buffer.from(utf8Text, 'utf8')
|
|
249
249
|
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()])
|
|
250
250
|
const authTag = cipher.getAuthTag()
|
|
251
251
|
return {
|
|
@@ -356,7 +356,7 @@ export function decryptConvergentCiphertext(raw, contentHashHex) {
|
|
|
356
356
|
decipher.setAuthTag(authTag)
|
|
357
357
|
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
358
358
|
const check = createHash('sha256').update(plain).digest('hex')
|
|
359
|
-
if (check !== contentHashHex
|
|
359
|
+
if (check !== contentHashHex) return null
|
|
360
360
|
return plain
|
|
361
361
|
}
|
|
362
362
|
catch { return null }
|
|
@@ -379,10 +379,9 @@ export function decryptRandomCiphertext(raw, contentKey, contentHashHex = '') {
|
|
|
379
379
|
const decipher = createDecipheriv('aes-256-gcm', Buffer.from(contentKey), iv)
|
|
380
380
|
decipher.setAuthTag(authTag)
|
|
381
381
|
const plain = Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
382
|
-
|
|
383
|
-
if (expect) {
|
|
382
|
+
if (contentHashHex) {
|
|
384
383
|
const check = createHash('sha256').update(plain).digest('hex')
|
|
385
|
-
if (check !==
|
|
384
|
+
if (check !== contentHashHex) return null
|
|
386
385
|
}
|
|
387
386
|
return plain
|
|
388
387
|
}
|
package/dag/canonicalize_row.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
|
|
|
28
28
|
canonicalizeHexField(out, key)
|
|
29
29
|
for (const key of entityHashKeys) {
|
|
30
30
|
if (!out[key]) continue
|
|
31
|
-
const entityHash = String(out[key])
|
|
31
|
+
const entityHash = String(out[key] || '')
|
|
32
32
|
if (!isEntityHash128(entityHash))
|
|
33
33
|
throw new Error(`${key} must be 128 hex characters`)
|
|
34
34
|
out[key] = entityHash
|
|
@@ -21,7 +21,7 @@ export function noteAdvertPeerHints(verifiedNodeHash, body, meta) {
|
|
|
21
21
|
// lanHosts 先写(差→优,unshift 后优在前),再写观测 address,使 address 最优先
|
|
22
22
|
for (const host of normalizeLanHosts(body?.lanHosts).toReversed())
|
|
23
23
|
noteLanPeerHint(verifiedNodeHash, { host, port: tcpPort })
|
|
24
|
-
const address = String(meta?.address || '')
|
|
24
|
+
const address = String(meta?.address || '')
|
|
25
25
|
if (address)
|
|
26
26
|
noteLanPeerHint(verifiedNodeHash, { host: address, port: tcpPort })
|
|
27
27
|
}
|
package/discovery/adverts.mjs
CHANGED
|
@@ -69,11 +69,9 @@ export function encryptAdvertForScope(scope, localIdentity, advertBody) {
|
|
|
69
69
|
* Untrusted ingress:解密并验签 advert;失败返回 null,不抛。不写入可见池 / peer hints。
|
|
70
70
|
* @param {string} rendezvousKey rendezvous 键
|
|
71
71
|
* @param {Uint8Array} bytes 加密 advert
|
|
72
|
-
* @param {object} [meta] 元数据
|
|
73
72
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
|
|
74
73
|
*/
|
|
75
|
-
export async function ingestEncryptedAdvert(rendezvousKey, bytes
|
|
76
|
-
void meta
|
|
74
|
+
export async function ingestEncryptedAdvert(rendezvousKey, bytes) {
|
|
77
75
|
const packet = decryptSignalPacket(rendezvousKey, bytes)
|
|
78
76
|
if (packet?.type !== 'advert' || !packet.body) return null
|
|
79
77
|
const verifiedNodeHash = await verifySignedAdvert(rendezvousKey, packet.body)
|
|
@@ -84,31 +82,28 @@ export async function ingestEncryptedAdvert(rendezvousKey, bytes, meta) {
|
|
|
84
82
|
/**
|
|
85
83
|
* Untrusted ingress:验签 network-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
86
84
|
* @param {Uint8Array} bytes 加密 advert
|
|
87
|
-
* @param {object} [meta] 元数据
|
|
88
85
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
|
|
89
86
|
*/
|
|
90
|
-
export async function ingestNetworkAdvert(bytes
|
|
91
|
-
return ingestEncryptedAdvert(networkRendezvousKey(), bytes
|
|
87
|
+
export async function ingestNetworkAdvert(bytes) {
|
|
88
|
+
return ingestEncryptedAdvert(networkRendezvousKey(), bytes)
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
/**
|
|
95
92
|
* Untrusted ingress:验签 node-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
96
93
|
* @param {string} nodeHash 目标 nodeHash
|
|
97
94
|
* @param {Uint8Array} bytes 加密 advert
|
|
98
|
-
* @param {object} [meta] 元数据
|
|
99
95
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
|
|
100
96
|
*/
|
|
101
|
-
export async function ingestNodeAdvert(nodeHash, bytes
|
|
102
|
-
return ingestEncryptedAdvert(nodeRendezvousKey(normalizeHex64(nodeHash)), bytes
|
|
97
|
+
export async function ingestNodeAdvert(nodeHash, bytes) {
|
|
98
|
+
return ingestEncryptedAdvert(nodeRendezvousKey(normalizeHex64(nodeHash)), bytes)
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
/**
|
|
106
102
|
* Untrusted ingress:验签 group-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
107
103
|
* @param {string} roomSecret 房间密钥
|
|
108
104
|
* @param {Uint8Array} bytes 加密 advert
|
|
109
|
-
* @param {object} [meta] 元数据
|
|
110
105
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash 与 advert body,否则 null
|
|
111
106
|
*/
|
|
112
|
-
export async function ingestGroupAdvert(roomSecret, bytes
|
|
113
|
-
return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes
|
|
107
|
+
export async function ingestGroupAdvert(roomSecret, bytes) {
|
|
108
|
+
return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes)
|
|
114
109
|
}
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -9,8 +9,10 @@ import { noteDiscoveryPeerClue } from '../peer_clue.mjs'
|
|
|
9
9
|
import { getBtPeerHint } from './peer_hints.mjs'
|
|
10
10
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* 重导出有关 BT 生命期的工具函数。
|
|
14
|
+
*/
|
|
15
|
+
export { canUseBluetoothRuntime, waitPoweredOn }
|
|
14
16
|
|
|
15
17
|
const BT_SERVICE_UUID = 'f017f017f017f017f017f017f017f017'
|
|
16
18
|
const BT_CHARACTERISTIC_UUID = 'f017f017f017f017f017f017f017f018'
|
|
@@ -53,13 +55,6 @@ export function clearBtVisibleNodes() {
|
|
|
53
55
|
visibleByHash.clear()
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
/**
|
|
57
|
-
* @returns {Promise<boolean>} 可用为 true
|
|
58
|
-
*/
|
|
59
|
-
export async function canUseBluetoothDiscovery() {
|
|
60
|
-
return canUseBluetoothRuntime()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
/**
|
|
64
59
|
* @param {Map<string, Uint8Array>} presence nodeHash → encrypted advert
|
|
65
60
|
* @returns {Buffer} JSON blob
|
|
@@ -100,9 +95,9 @@ function parsePresenceBlob(raw) {
|
|
|
100
95
|
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
|
|
101
96
|
*/
|
|
102
97
|
export async function acceptBtScannedPresence(bytes, meta) {
|
|
103
|
-
const peripheralId = String(meta?.peripheralId || '')
|
|
98
|
+
const peripheralId = String(meta?.peripheralId || '')
|
|
104
99
|
if (!peripheralId || !bytes?.byteLength) return null
|
|
105
|
-
const ingested = await ingestNetworkAdvert(bytes
|
|
100
|
+
const ingested = await ingestNetworkAdvert(bytes)
|
|
106
101
|
if (!ingested) return null
|
|
107
102
|
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
108
103
|
noteBtVisibleNode(ingested.verifiedNodeHash)
|
|
@@ -15,9 +15,8 @@ const hints = createTtlMap(BT_PEER_HINT_TTL_MS)
|
|
|
15
15
|
*/
|
|
16
16
|
export function noteBtPeerHint(nodeHash, peripheralId) {
|
|
17
17
|
const hash = normalizeHex64(nodeHash)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
hints.set(hash, { peripheralId: id })
|
|
18
|
+
if (!hash || !peripheralId) return
|
|
19
|
+
hints.set(hash, { peripheralId })
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
package/discovery/bt/runtime.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export async function loadBleno() {
|
|
|
96
96
|
if (probeBluetoothHardware() === false)
|
|
97
97
|
throw new Error('p2p: no bluetooth adapter')
|
|
98
98
|
const mod = await import('@stoprocent/bleno')
|
|
99
|
-
if (
|
|
99
|
+
if (mod.withBindings) return mod.withBindings('default')
|
|
100
100
|
return mod.default ?? mod
|
|
101
101
|
}
|
|
102
102
|
|
package/discovery/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ export function setDiscoveryLinkDialer(dialer) {
|
|
|
27
27
|
*/
|
|
28
28
|
export function registerDiscoveryProvider(provider) {
|
|
29
29
|
if (!provider?.id) throw new Error('p2p: discovery provider requires id')
|
|
30
|
-
providers.set(
|
|
30
|
+
providers.set(provider.id, provider)
|
|
31
31
|
return () => unregisterDiscoveryProvider(provider.id)
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -36,8 +36,8 @@ export function registerDiscoveryProvider(provider) {
|
|
|
36
36
|
* @returns {void}
|
|
37
37
|
*/
|
|
38
38
|
export function unregisterDiscoveryProvider(id) {
|
|
39
|
-
const provider = providers.get(
|
|
40
|
-
providers.delete(
|
|
39
|
+
const provider = providers.get(id)
|
|
40
|
+
providers.delete(id)
|
|
41
41
|
try { provider?.dispose?.() } catch { /* ignore */ }
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -59,7 +59,7 @@ export function listDiscoveryProviders() {
|
|
|
59
59
|
* @returns {DiscoveryProvider | undefined} 对应 id 的提供者,未注册为 undefined
|
|
60
60
|
*/
|
|
61
61
|
export function getDiscoveryProvider(id) {
|
|
62
|
-
return providers.get(
|
|
62
|
+
return providers.get(id)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
@@ -67,17 +67,17 @@ export function getDiscoveryProvider(id) {
|
|
|
67
67
|
* @returns {DiscoveryProvider[]} 实现了指定方法的提供者列表
|
|
68
68
|
*/
|
|
69
69
|
function providersWith(method) {
|
|
70
|
-
return listDiscoveryProviders().filter(provider =>
|
|
70
|
+
return listDiscoveryProviders().filter(provider => provider[method])
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* @param {Array<(
|
|
74
|
+
* @param {Array<() => void>} cleanups 清理函数
|
|
75
75
|
* @returns {() => void} 调用时依次执行各 cleanup
|
|
76
76
|
*/
|
|
77
77
|
function composeCleanups(cleanups) {
|
|
78
78
|
return () => {
|
|
79
79
|
for (const stop of cleanups)
|
|
80
|
-
|
|
80
|
+
try { stop() } catch { /* ignore */ }
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -92,15 +92,15 @@ async function fanInProviderMethod(method, methodArguments, options = {}) {
|
|
|
92
92
|
const requireAny = options.requireAny !== false
|
|
93
93
|
const capable = providersWith(method)
|
|
94
94
|
if (requireAny && !capable.length) throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
|
|
95
|
-
const cleanups = await Promise.all(capable.map(async provider => {
|
|
95
|
+
const cleanups = (await Promise.all(capable.map(async provider => {
|
|
96
96
|
try {
|
|
97
97
|
return await provider[method](...methodArguments)
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
100
|
return null
|
|
101
101
|
}
|
|
102
|
-
}))
|
|
103
|
-
if (requireAny && !cleanups.
|
|
102
|
+
}))).filter(Boolean)
|
|
103
|
+
if (requireAny && !cleanups.length)
|
|
104
104
|
throw new Error(options.errorLabel || `p2p: ${method} unavailable`)
|
|
105
105
|
return composeCleanups(cleanups)
|
|
106
106
|
}
|
|
@@ -116,7 +116,7 @@ export async function listVisibleNodeHashes(options = {}) {
|
|
|
116
116
|
/** @type {Record<string, string[]>} */
|
|
117
117
|
const byProvider = {}
|
|
118
118
|
for (const provider of listDiscoveryProviders()) {
|
|
119
|
-
if (
|
|
119
|
+
if (!provider.listVisibleNodeHashes) continue
|
|
120
120
|
try {
|
|
121
121
|
const hashes = await provider.listVisibleNodeHashes(options)
|
|
122
122
|
byProvider[provider.id] = hashes.map(hash => shortHash(hash))
|
|
@@ -151,7 +151,7 @@ export async function listVisibleNodeHashes(options = {}) {
|
|
|
151
151
|
export async function prepareConnectToNode(nodeHash, options = {}) {
|
|
152
152
|
const hash = normalizeHex64(nodeHash)
|
|
153
153
|
for (const provider of listDiscoveryProviders()) {
|
|
154
|
-
if (
|
|
154
|
+
if (!provider.connectToNode) continue
|
|
155
155
|
try { await provider.connectToNode(hash, options) }
|
|
156
156
|
catch { /* prepare next medium */ }
|
|
157
157
|
}
|
|
@@ -304,7 +304,7 @@ export function noteVisibleNodeFromAdvert(verifiedNodeHash, options = {}) {
|
|
|
304
304
|
*/
|
|
305
305
|
export async function watchVerifiedNodeAdvert(nodeHash, onAdvert) {
|
|
306
306
|
return await watchNodeAdvert(nodeHash, async (bytes, meta) => {
|
|
307
|
-
const ingested = await ingestNodeAdvert(nodeHash, bytes
|
|
307
|
+
const ingested = await ingestNodeAdvert(nodeHash, bytes)
|
|
308
308
|
if (!ingested) return
|
|
309
309
|
noteVisibleNodeFromAdvert(ingested.verifiedNodeHash)
|
|
310
310
|
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
|
|
@@ -319,7 +319,7 @@ export async function watchVerifiedNodeAdvert(nodeHash, onAdvert) {
|
|
|
319
319
|
*/
|
|
320
320
|
export async function watchVerifiedGroupAdverts(roomSecret, onAdvert) {
|
|
321
321
|
return await watchGroupAdverts(roomSecret, async (bytes, meta) => {
|
|
322
|
-
const ingested = await ingestGroupAdvert(roomSecret, bytes
|
|
322
|
+
const ingested = await ingestGroupAdvert(roomSecret, bytes)
|
|
323
323
|
if (!ingested) return
|
|
324
324
|
noteVisibleNodeFromAdvert(ingested.verifiedNodeHash, { roomSecret })
|
|
325
325
|
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body, meta))
|
package/discovery/lan.mjs
CHANGED
|
@@ -59,7 +59,7 @@ export function clearLanVisibleNodes() {
|
|
|
59
59
|
*/
|
|
60
60
|
export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
61
61
|
if (!advertBytes?.byteLength) return null
|
|
62
|
-
const ingested = await ingestNetworkAdvert(advertBytes
|
|
62
|
+
const ingested = await ingestNetworkAdvert(advertBytes)
|
|
63
63
|
if (!ingested) return null
|
|
64
64
|
const skipHash = meta.skipNodeHash ? normalizeHex64(meta.skipNodeHash) : null
|
|
65
65
|
if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
|
|
@@ -68,7 +68,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
|
68
68
|
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
69
69
|
if (firstSeen) {
|
|
70
70
|
noteDiscoveryPeerClue(ingested.verifiedNodeHash)
|
|
71
|
-
const host = String(meta.address || '')
|
|
71
|
+
const host = String(meta.address || '')
|
|
72
72
|
nodeDebug('p2p:lan peer visible', {
|
|
73
73
|
peer: shortHash(ingested.verifiedNodeHash),
|
|
74
74
|
host: host || undefined,
|
|
@@ -85,7 +85,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
|
85
85
|
*/
|
|
86
86
|
export function createLanDiscoveryProvider(options = {}) {
|
|
87
87
|
const port = Number(options.port) || DEFAULT_PORT
|
|
88
|
-
const group =
|
|
88
|
+
const group = options.group || DEFAULT_GROUP
|
|
89
89
|
/** @type {import('node:dgram').Socket | null} */
|
|
90
90
|
let socket = null
|
|
91
91
|
let bound = false
|