@steve02081504/fount-p2p 0.0.35 → 0.0.37
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/AGENTS.md +2 -2
- package/core/entity_id.mjs +6 -6
- package/core/entity_id_parse.mjs +7 -7
- package/core/hexIds.mjs +6 -18
- package/crypto/checkpoint_sign.mjs +4 -4
- package/dag/canonicalize_row.mjs +4 -7
- package/discovery/adverts.mjs +17 -10
- package/discovery/bt/index.mjs +11 -11
- package/discovery/bt/peer_hints.mjs +4 -7
- package/discovery/index.mjs +8 -12
- package/discovery/internal/signal_crypto.mjs +1 -2
- package/discovery/lan.mjs +7 -9
- package/discovery/lan_peer_hints.mjs +5 -8
- package/discovery/nostr/census.mjs +281 -0
- package/discovery/nostr/census_math.mjs +54 -0
- package/discovery/nostr/constants.mjs +43 -0
- package/discovery/nostr/index.mjs +612 -0
- package/discovery/nostr/relays.mjs +1071 -0
- package/discovery/nostr/selection.mjs +224 -0
- package/discovery/nostr/session.mjs +584 -0
- package/discovery/peer_clue.mjs +1 -4
- package/docs/nostr_relay_discovery.md +92 -0
- package/federation/entity_key_chain.mjs +10 -9
- package/federation/message_rate_limit.mjs +1 -3
- package/files/manifest/fetch.mjs +4 -6
- package/files/manifest/normalize.mjs +2 -2
- package/files/manifest/public.mjs +5 -5
- package/governance/branch.mjs +1 -2
- package/governance/join_pow.mjs +2 -4
- package/governance/owner_succession_ballot.mjs +5 -5
- package/index.mjs +11 -4
- package/infra/priority.mjs +1 -2
- package/link/handshake.mjs +153 -41
- package/link/pipe.mjs +1 -2
- package/link/providers/ble_gatt.mjs +2 -3
- package/link/providers/lan_tcp.mjs +2 -3
- package/link/providers/link_id_pipe.mjs +3 -3
- package/link/providers/{nostr.mjs → nostr/index.mjs} +22 -17
- package/mailbox/deliver_or_store.mjs +11 -12
- package/mailbox/prune.mjs +3 -4
- package/mailbox/rate.mjs +3 -2
- package/mailbox/store.mjs +6 -9
- package/node/denylist.mjs +25 -28
- package/node/feature_config.mjs +30 -0
- package/node/identity.mjs +6 -6
- package/node/instance.mjs +21 -0
- package/node/network.mjs +12 -12
- package/node/personal_block.mjs +4 -4
- package/node/reputation_store.mjs +13 -14
- package/node/reputation_sync.mjs +9 -10
- package/node/storage.mjs +23 -0
- package/package.json +10 -4
- package/pages/app.mjs +172 -0
- package/pages/index.html +36 -0
- package/reputation/engine.mjs +13 -16
- package/schemas/discovery.mjs +5 -6
- package/schemas/federation_pull.mjs +7 -7
- package/schemas/mailbox.mjs +3 -3
- package/schemas/part_query.mjs +5 -5
- package/timeline/verify_remote.mjs +7 -7
- package/transport/link_registry.mjs +16 -19
- package/transport/mesh_keepalive.mjs +8 -11
- package/transport/offer_answer.mjs +3 -3
- package/transport/peer_health.mjs +12 -15
- package/transport/runtime_bootstrap.mjs +39 -3
- package/trust_graph/send.mjs +3 -3
- package/discovery/nostr.mjs +0 -1074
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
**Facade:** `index.mjs`; subpath exports mirror directories.
|
|
16
16
|
|
|
17
|
-
Detail docs: [transports](docs/transports.md) · [mesh](docs/mesh.md) · [signaling](docs/signaling.md) · [runtime](docs/runtime.md) · [infra](docs/infra.md) · [wire](docs/wire.md) · [evfs](docs/evfs.md) · [reputation](docs/reputation.md)
|
|
17
|
+
Detail docs: [transports](docs/transports.md) · [mesh](docs/mesh.md) · [signaling](docs/signaling.md) · [nostr-relay-discovery](docs/nostr_relay_discovery.md) · [runtime](docs/runtime.md) · [infra](docs/infra.md) · [wire](docs/wire.md) · [evfs](docs/evfs.md) · [reputation](docs/reputation.md)
|
|
18
18
|
|
|
19
19
|
### Runtime: isomorphic vs Node
|
|
20
20
|
|
|
@@ -33,7 +33,7 @@ Deno / native / BT: [runtime.md](docs/runtime.md).
|
|
|
33
33
|
- **Heterogeneous backends:** normalize at the load boundary (e.g. `link/rtc/ice_local_hostname.mjs` wraps W3C RTC backends); call sites speak one contract.
|
|
34
34
|
- **File naming:** parent directory is scope — short child names. Tunables default `<dir>/tunables.json` (exception: `schemas/part_query.tunables.json`). Subpath `package.json` exports mirror filenames.
|
|
35
35
|
- **Import boundary:** `test/integration/p2p_shell_import_guard.test.mjs`.
|
|
36
|
-
- **No scattered `trim` / `toLowerCase`:** hex IDs must already be lowercase
|
|
36
|
+
- **No scattered `trim` / `toLowerCase`:** hex IDs must already be lowercase and without a `0x` prefix — a `0x`-prefixed, mixed-case, or whitespace value is rejected by `isHex64`/`isEntityHash128`, never cleaned. Exceptions: JSONL blank lines, SDP fingerprint, CLI/`scripts` parsing.
|
|
37
37
|
- **No `String(x)` / `x || ''` on typed `string`:** if `@param {string}`, use it directly; `String(...)` / `|| ''` / `?? ''` only at optional / `unknown` / disk / inbound boundaries, or number→string.
|
|
38
38
|
- **Optional methods:** `if (fn) return await fn(...)` / `if (fn) …` — never `typeof x === 'function'`.
|
|
39
39
|
|
package/core/entity_id.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isEntityHash128,
|
|
8
8
|
parseEntityHash,
|
|
9
9
|
} from './entity_id_parse.mjs'
|
|
10
|
-
import { isHex64
|
|
10
|
+
import { isHex64 } from './hexIds.mjs'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* entityHash 编解码与校验(re-export)。
|
|
@@ -24,8 +24,8 @@ export {
|
|
|
24
24
|
* @returns {string} 64 位 nodeHash / subjectHash(pubKeyHash)
|
|
25
25
|
*/
|
|
26
26
|
export function hashFromPubKeyHex(pubKeyHex) {
|
|
27
|
-
const hex =
|
|
28
|
-
if (!
|
|
27
|
+
const hex = isHex64(pubKeyHex)
|
|
28
|
+
if (!hex) throw new Error('invalid pubKeyHex')
|
|
29
29
|
const bytes = hexToBytes(hex)
|
|
30
30
|
if (bytes.length !== 32) throw new Error('invalid pubKeyHex')
|
|
31
31
|
return pubKeyHash(bytes)
|
|
@@ -46,8 +46,8 @@ export function entityHashFromRecoveryPubKeyHex(nodeHash, recoveryPubKeyHex) {
|
|
|
46
46
|
* @returns {string} entityHash
|
|
47
47
|
*/
|
|
48
48
|
export function entityHashFromSubjectHash(nodeHash, subjectHash) {
|
|
49
|
-
const node =
|
|
50
|
-
const subject =
|
|
51
|
-
if (!
|
|
49
|
+
const node = isHex64(nodeHash)
|
|
50
|
+
const subject = isHex64(subjectHash)
|
|
51
|
+
if (!node || !subject) throw new Error('invalid subject hash')
|
|
52
52
|
return encodeEntityHash(node, subject)
|
|
53
53
|
}
|
package/core/entity_id_parse.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { isHex64
|
|
1
|
+
import { isHex64 } from './hexIds.mjs'
|
|
2
2
|
|
|
3
3
|
/** 128 位小写 hex:`nodeHash(64)` + `subjectHash(64)`。 */
|
|
4
4
|
export const ENTITY_HASH_RE = /^[\da-f]{128}$/u
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {unknown} value 待校验值
|
|
8
|
-
* @returns {
|
|
8
|
+
* @returns {string | null} 合法时返回原 128 位实体hash,否则 null(含 0x 前缀/大写/空白)
|
|
9
9
|
*/
|
|
10
10
|
export function isEntityHash128(value) {
|
|
11
|
-
return ENTITY_HASH_RE.test(
|
|
11
|
+
return ENTITY_HASH_RE.test(value) ? value : null
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -16,7 +16,7 @@ export function isEntityHash128(value) {
|
|
|
16
16
|
* @returns {{ entityHash: string, nodeHash: string, subjectHash: string } | null} 解析结果;非法时 null
|
|
17
17
|
*/
|
|
18
18
|
export function parseEntityHash(entityHash) {
|
|
19
|
-
const raw = String(entityHash ?? '')
|
|
19
|
+
const raw = String(entityHash ?? '')
|
|
20
20
|
if (!ENTITY_HASH_RE.test(raw)) return null
|
|
21
21
|
return {
|
|
22
22
|
entityHash: raw,
|
|
@@ -31,9 +31,9 @@ export function parseEntityHash(entityHash) {
|
|
|
31
31
|
* @returns {string} 128 位 entityHash
|
|
32
32
|
*/
|
|
33
33
|
export function encodeEntityHash(nodeHash, subjectHash) {
|
|
34
|
-
const node =
|
|
35
|
-
const subject =
|
|
36
|
-
if (!
|
|
34
|
+
const node = isHex64(nodeHash)
|
|
35
|
+
const subject = isHex64(subjectHash)
|
|
36
|
+
if (!node || !subject)
|
|
37
37
|
throw new Error('invalid entity hash parts')
|
|
38
38
|
return node + subject
|
|
39
39
|
}
|
package/core/hexIds.mjs
CHANGED
|
@@ -10,21 +10,12 @@ export const BLOB_STORAGE_LOCATOR_RE = /^blob:([\da-f]{64})$/u
|
|
|
10
10
|
/** `local:…/chunks/<64hex>.bin` 群分块路径。 */
|
|
11
11
|
export const LOCAL_CHUNK_FILE_RE = /^local:[^/]+\/chunks\/([\da-f]{64})\.bin$/u
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* @param {unknown} value 原始字符串
|
|
15
|
-
* @returns {string} 去可选 0x 前缀后的字符串(不修大小写/空白)
|
|
16
|
-
*/
|
|
17
|
-
export function normalizeHex64(value) {
|
|
18
|
-
return String(value ?? '').replace(/^0x/iu, '')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
/**
|
|
22
14
|
* @param {unknown} value 待校验值
|
|
23
|
-
* @returns {string | null}
|
|
15
|
+
* @returns {string | null} 合法时返回原 64 位 hex,否则 null(含 0x 前缀/大写/空白)
|
|
24
16
|
*/
|
|
25
17
|
export function isHex64(value) {
|
|
26
|
-
|
|
27
|
-
return HEX_ID_64.test(normalized) ? normalized : null
|
|
18
|
+
return HEX_ID_64.test(value) ? value : null
|
|
28
19
|
}
|
|
29
20
|
|
|
30
21
|
/**
|
|
@@ -34,22 +25,19 @@ export function isHex64(value) {
|
|
|
34
25
|
* @returns {number} 排序比较结果
|
|
35
26
|
*/
|
|
36
27
|
export function compareHex64Asc(a, b) {
|
|
37
|
-
|
|
38
|
-
const sb = normalizeHex64(b)
|
|
39
|
-
return sa < sb ? -1 : sa > sb ? 1 : 0
|
|
28
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
40
29
|
}
|
|
41
30
|
|
|
42
31
|
/**
|
|
43
|
-
*
|
|
32
|
+
* 外部入站专用:断言小写 64 位 hex(不清理 0x 前缀,直接拒绝)。
|
|
44
33
|
* @param {unknown} value 原始值
|
|
45
34
|
* @param {string} [label='hex64'] 字段名(错误信息)
|
|
46
35
|
* @returns {string} 小写 64 位 hex
|
|
47
36
|
*/
|
|
48
37
|
export function assertHex64(value, label = 'hex64') {
|
|
49
|
-
|
|
50
|
-
if (!HEX_ID_64.test(normalized))
|
|
38
|
+
if (!HEX_ID_64.test(value))
|
|
51
39
|
throw new Error(`${label} must be 64 hex characters`)
|
|
52
|
-
return
|
|
40
|
+
return value
|
|
53
41
|
}
|
|
54
42
|
|
|
55
43
|
/**
|
|
@@ -27,8 +27,8 @@ export async function signCheckpoint(payload, secretKey) {
|
|
|
27
27
|
* @returns {Promise<boolean>} 合法为 true
|
|
28
28
|
*/
|
|
29
29
|
export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
|
|
30
|
-
const raw =
|
|
31
|
-
if (!
|
|
30
|
+
const raw = isSignatureHex128(checkpoint.checkpoint_signature)
|
|
31
|
+
if (!raw) return false
|
|
32
32
|
const body = { ...checkpoint }
|
|
33
33
|
delete body.checkpoint_signature
|
|
34
34
|
const messageBytes = Buffer.from(canonicalStringify(body), 'utf8')
|
|
@@ -41,7 +41,7 @@ export async function verifyCheckpointSignature(checkpoint, ownerPublicKey) {
|
|
|
41
41
|
* @returns {boolean} 签名格式合法为 true
|
|
42
42
|
*/
|
|
43
43
|
export function isSignedCheckpoint(checkpoint) {
|
|
44
|
-
return !!isSignatureHex128(
|
|
44
|
+
return !!isSignatureHex128(checkpoint?.checkpoint_signature)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -60,7 +60,7 @@ export async function verifyRemoteCheckpoint(checkpoint) {
|
|
|
60
60
|
const ownerHash = checkpoint.members_record?.delegatedOwnerPubKeyHash
|
|
61
61
|
const owner = checkpoint.members_record?.members?.[ownerHash]
|
|
62
62
|
const pubHex = owner?.pubKeyHex
|
|
63
|
-
if (!
|
|
63
|
+
if (!isHex64(pubHex))
|
|
64
64
|
return { valid: false, reason: 'delegated owner pubkey missing' }
|
|
65
65
|
if (!await verifyCheckpointSignature(checkpoint, Buffer.from(pubHex, 'hex')))
|
|
66
66
|
return { valid: false, reason: 'checkpoint signature invalid' }
|
package/dag/canonicalize_row.mjs
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
* DAG / 时间线签名行入库前的共用 hex 规范化(非权限校验)。
|
|
3
3
|
*/
|
|
4
4
|
import { isEntityHash128 } from '../core/entity_id.mjs'
|
|
5
|
-
import { assertHex64
|
|
5
|
+
import { assertHex64 } from '../core/hexIds.mjs'
|
|
6
6
|
/**
|
|
7
7
|
* @param {Record<string, unknown>} obj 可变对象
|
|
8
8
|
* @param {string} key 字段名
|
|
9
9
|
*/
|
|
10
10
|
function canonicalizeHexField(obj, key) {
|
|
11
11
|
if (!obj[key]) return
|
|
12
|
-
|
|
13
|
-
if (!HEX_ID_64.test(normalized))
|
|
14
|
-
throw new Error(`${key} must be 64 hex characters`)
|
|
15
|
-
obj[key] = normalized
|
|
12
|
+
obj[key] = assertHex64(obj[key], key)
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
/**
|
|
@@ -28,8 +25,8 @@ export function canonicalizeRowContent(content, hexKeys, entityHashKeys = new Se
|
|
|
28
25
|
canonicalizeHexField(out, key)
|
|
29
26
|
for (const key of entityHashKeys) {
|
|
30
27
|
if (!out[key]) continue
|
|
31
|
-
const entityHash =
|
|
32
|
-
if (!
|
|
28
|
+
const entityHash = isEntityHash128(out[key])
|
|
29
|
+
if (!entityHash)
|
|
33
30
|
throw new Error(`${key} must be 128 hex characters`)
|
|
34
31
|
out[key] = entityHash
|
|
35
32
|
}
|
package/discovery/adverts.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
1
|
import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
|
|
3
2
|
|
|
4
3
|
import {
|
|
@@ -30,9 +29,10 @@ export function rendezvousKeyForScope(scope, selfNodeHash) {
|
|
|
30
29
|
* @param {AdvertScope} scope advert 域
|
|
31
30
|
* @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} localIdentity 本地身份
|
|
32
31
|
* @param {number | null | undefined} [tcpPort] LAN TCP 端口
|
|
32
|
+
* @param {{ pool?: Array<{ url: string, rtt?: number }>, listen?: string[] }} [relayData] 已规范化并经 sanitize 裁剪的 relay 字段
|
|
33
33
|
* @returns {Promise<object>} 签名 advert body
|
|
34
34
|
*/
|
|
35
|
-
export async function buildSignedAdvertForScope(scope, localIdentity, tcpPort) {
|
|
35
|
+
export async function buildSignedAdvertForScope(scope, localIdentity, tcpPort, relayData) {
|
|
36
36
|
const key = rendezvousKeyForScope(scope, localIdentity.nodeHash)
|
|
37
37
|
const lanHosts = scope === 'network' && tcpPort != null
|
|
38
38
|
? listMulticastIpv4Addresses()
|
|
@@ -41,6 +41,8 @@ export async function buildSignedAdvertForScope(scope, localIdentity, tcpPort) {
|
|
|
41
41
|
...localIdentity,
|
|
42
42
|
...tcpPort != null ? { tcpPort } : {},
|
|
43
43
|
...lanHosts.length ? { lanHosts } : {},
|
|
44
|
+
...relayData?.pool ? { nostrRelayPool: relayData.pool } : {},
|
|
45
|
+
...relayData?.listen ? { listenNostrRelays: relayData.listen } : {},
|
|
44
46
|
})
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -69,20 +71,25 @@ export function encryptAdvertForScope(scope, localIdentity, advertBody) {
|
|
|
69
71
|
* Untrusted ingress:解密并验签 advert;失败返回 null,不抛。不写入可见池 / peer hints。
|
|
70
72
|
* @param {string} rendezvousKey rendezvous 键
|
|
71
73
|
* @param {Uint8Array} bytes 加密 advert
|
|
72
|
-
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash
|
|
74
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object, relayPool: Array<{ url: string, rtt: number }>, listenRelays: string[] } | null>} 验签成功返回 nodeHash、advert body 与规范化 relay 字段,否则 null
|
|
73
75
|
*/
|
|
74
76
|
export async function ingestEncryptedAdvert(rendezvousKey, bytes) {
|
|
75
77
|
const packet = decryptSignalPacket(rendezvousKey, bytes)
|
|
76
78
|
if (packet?.type !== 'advert' || !packet.body) return null
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
return {
|
|
79
|
+
const verified = await verifySignedAdvert(rendezvousKey, packet.body)
|
|
80
|
+
if (!verified) return null
|
|
81
|
+
return {
|
|
82
|
+
verifiedNodeHash: verified.nodeHash,
|
|
83
|
+
body: packet.body,
|
|
84
|
+
relayPool: verified.relayPool,
|
|
85
|
+
listenRelays: verified.listenRelays,
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
90
|
* Untrusted ingress:验签 network-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
84
91
|
* @param {Uint8Array} bytes 加密 advert
|
|
85
|
-
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash
|
|
92
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object, relayPool: Array<{ url: string, rtt: number }>, listenRelays: string[] } | null>} 验签成功返回 nodeHash、advert body 与规范化 relay 字段,否则 null
|
|
86
93
|
*/
|
|
87
94
|
export async function ingestNetworkAdvert(bytes) {
|
|
88
95
|
return ingestEncryptedAdvert(networkRendezvousKey(), bytes)
|
|
@@ -92,17 +99,17 @@ export async function ingestNetworkAdvert(bytes) {
|
|
|
92
99
|
* Untrusted ingress:验签 node-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
93
100
|
* @param {string} nodeHash 目标 nodeHash
|
|
94
101
|
* @param {Uint8Array} bytes 加密 advert
|
|
95
|
-
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash
|
|
102
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object, relayPool: Array<{ url: string, rtt: number }>, listenRelays: string[] } | null>} 验签成功返回 nodeHash、advert body 与规范化 relay 字段,否则 null
|
|
96
103
|
*/
|
|
97
104
|
export async function ingestNodeAdvert(nodeHash, bytes) {
|
|
98
|
-
return ingestEncryptedAdvert(nodeRendezvousKey(
|
|
105
|
+
return ingestEncryptedAdvert(nodeRendezvousKey(nodeHash), bytes)
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
/**
|
|
102
109
|
* Untrusted ingress:验签 group-scope advert;失败返回 null。不写盘 / 不写 hints。
|
|
103
110
|
* @param {string} roomSecret 房间密钥
|
|
104
111
|
* @param {Uint8Array} bytes 加密 advert
|
|
105
|
-
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签成功返回 nodeHash
|
|
112
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object, relayPool: Array<{ url: string, rtt: number }>, listenRelays: string[] } | null>} 验签成功返回 nodeHash、advert body 与规范化 relay 字段,否则 null
|
|
106
113
|
*/
|
|
107
114
|
export async function ingestGroupAdvert(roomSecret, bytes) {
|
|
108
115
|
return ingestEncryptedAdvert(groupRendezvousKey(roomSecret), bytes)
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
|
-
import { isHex64
|
|
3
|
+
import { isHex64 } from '../../core/hexIds.mjs'
|
|
4
4
|
import { nodeDebug, shortHash } from '../../node/log.mjs'
|
|
5
5
|
import { noteAdvertPeerHints } from '../advert_peer_hints.mjs'
|
|
6
6
|
import { ingestNetworkAdvert } from '../adverts.mjs'
|
|
@@ -31,8 +31,8 @@ const visibleByHash = new Map()
|
|
|
31
31
|
* @returns {void}
|
|
32
32
|
*/
|
|
33
33
|
export function noteBtVisibleNode(nodeHash, now = Date.now()) {
|
|
34
|
-
const hash =
|
|
35
|
-
if (!
|
|
34
|
+
const hash = isHex64(nodeHash)
|
|
35
|
+
if (!hash) return
|
|
36
36
|
visibleByHash.set(hash, now)
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -176,7 +176,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
176
176
|
onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
|
|
177
177
|
try {
|
|
178
178
|
const parsed = JSON.parse(Buffer.from(data).toString('utf8'))
|
|
179
|
-
const to =
|
|
179
|
+
const to = parsed?.to
|
|
180
180
|
const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
|
|
181
181
|
if (isHex64(to) && bytes.byteLength)
|
|
182
182
|
for (const listener of signalListeners.get(to) || [])
|
|
@@ -268,7 +268,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
268
268
|
* @returns {Promise<boolean>} 是否经 GATT 发出
|
|
269
269
|
*/
|
|
270
270
|
async function sendNodeSignalViaGatt(toNodeHash, bytes) {
|
|
271
|
-
const hash =
|
|
271
|
+
const hash = toNodeHash
|
|
272
272
|
const hint = getBtPeerHint(hash)
|
|
273
273
|
if (!hint) return false
|
|
274
274
|
const blob = Buffer.from(JSON.stringify({
|
|
@@ -353,8 +353,8 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
353
353
|
* @returns {Promise<boolean>} 有 BT hint 且 GATT 可达时为 true
|
|
354
354
|
*/
|
|
355
355
|
async connectToNode(nodeHash) {
|
|
356
|
-
const hash =
|
|
357
|
-
if (!
|
|
356
|
+
const hash = isHex64(nodeHash)
|
|
357
|
+
if (!hash) return false
|
|
358
358
|
if (!getBtPeerHint(hash)) return false
|
|
359
359
|
return await sendNodeSignalViaGatt(hash, new Uint8Array([0])).catch(() => false)
|
|
360
360
|
},
|
|
@@ -370,8 +370,8 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
370
370
|
const refresh = async () => {
|
|
371
371
|
const body = await getBeacon?.()
|
|
372
372
|
if (!body?.nodeHash || !body.advertBytes?.byteLength) return
|
|
373
|
-
const hash =
|
|
374
|
-
if (!
|
|
373
|
+
const hash = isHex64(body.nodeHash)
|
|
374
|
+
if (!hash) return
|
|
375
375
|
localNodeHash = hash
|
|
376
376
|
localPresence.set(hash, Uint8Array.from(body.advertBytes))
|
|
377
377
|
noteBtVisibleNode(hash)
|
|
@@ -401,8 +401,8 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
401
401
|
*/
|
|
402
402
|
async listenNodeSignals(localNodeHash, onSignal) {
|
|
403
403
|
if (role === 'scan') return () => { }
|
|
404
|
-
const hash =
|
|
405
|
-
if (!
|
|
404
|
+
const hash = isHex64(localNodeHash)
|
|
405
|
+
if (!hash) throw new Error('p2p: invalid nodeHash')
|
|
406
406
|
await ensurePeripheralRuntime()
|
|
407
407
|
if (!signalListeners.has(hash)) signalListeners.set(hash, new Set())
|
|
408
408
|
signalListeners.get(hash).add(onSignal)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
2
1
|
import { createTtlMap } from '../../utils/ttl_map.mjs'
|
|
3
2
|
|
|
4
3
|
/** BT peer hint 存活时间。 */
|
|
@@ -14,9 +13,8 @@ const hints = createTtlMap(BT_PEER_HINT_TTL_MS)
|
|
|
14
13
|
* @returns {void}
|
|
15
14
|
*/
|
|
16
15
|
export function noteBtPeerHint(nodeHash, peripheralId) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
hints.set(hash, { peripheralId })
|
|
16
|
+
if (!nodeHash || !peripheralId) return
|
|
17
|
+
hints.set(nodeHash, { peripheralId })
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
/**
|
|
@@ -26,9 +24,8 @@ export function noteBtPeerHint(nodeHash, peripheralId) {
|
|
|
26
24
|
* @returns {{ peripheralId: string } | null} hint 或 null
|
|
27
25
|
*/
|
|
28
26
|
export function getBtPeerHint(nodeHash, now = Date.now()) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return hints.get(hash, now)
|
|
27
|
+
if (!nodeHash) return null
|
|
28
|
+
return hints.get(nodeHash, now)
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
/**
|
package/discovery/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
1
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
3
2
|
|
|
4
3
|
import { ingestGroupAdvert, ingestNodeAdvert } from './adverts.mjs'
|
|
@@ -149,10 +148,9 @@ export async function listVisibleNodeHashes(options = {}) {
|
|
|
149
148
|
* @returns {Promise<void>}
|
|
150
149
|
*/
|
|
151
150
|
export async function prepareConnectToNode(nodeHash, options = {}) {
|
|
152
|
-
const hash = normalizeHex64(nodeHash)
|
|
153
151
|
for (const provider of listDiscoveryProviders()) {
|
|
154
152
|
if (!provider.connectToNode) continue
|
|
155
|
-
try { await provider.connectToNode(
|
|
153
|
+
try { await provider.connectToNode(nodeHash, options) }
|
|
156
154
|
catch { /* prepare next medium */ }
|
|
157
155
|
}
|
|
158
156
|
}
|
|
@@ -164,20 +162,19 @@ export async function prepareConnectToNode(nodeHash, options = {}) {
|
|
|
164
162
|
* @returns {Promise<boolean>} 是否建链成功
|
|
165
163
|
*/
|
|
166
164
|
export async function connectToNode(nodeHash, options = {}) {
|
|
167
|
-
const hash = normalizeHex64(nodeHash)
|
|
168
165
|
if (!linkDialer) {
|
|
169
|
-
await prepareConnectToNode(
|
|
170
|
-
nodeDebug('p2p:discovery connect skip', { peer: shortHash(
|
|
166
|
+
await prepareConnectToNode(nodeHash, options)
|
|
167
|
+
nodeDebug('p2p:discovery connect skip', { peer: shortHash(nodeHash), reason: 'no-dialer' })
|
|
171
168
|
return false
|
|
172
169
|
}
|
|
173
170
|
try {
|
|
174
|
-
const ok = !!await linkDialer(
|
|
175
|
-
nodeDebug(ok ? 'p2p:discovery connect ok' : 'p2p:discovery connect miss', { peer: shortHash(
|
|
171
|
+
const ok = !!await linkDialer(nodeHash)
|
|
172
|
+
nodeDebug(ok ? 'p2p:discovery connect ok' : 'p2p:discovery connect miss', { peer: shortHash(nodeHash) })
|
|
176
173
|
return ok
|
|
177
174
|
}
|
|
178
175
|
catch (error) {
|
|
179
176
|
nodeDebug('p2p:discovery connect fail', {
|
|
180
|
-
peer: shortHash(
|
|
177
|
+
peer: shortHash(nodeHash),
|
|
181
178
|
err: String(error?.message || error),
|
|
182
179
|
})
|
|
183
180
|
return false
|
|
@@ -271,8 +268,7 @@ export async function startGroupPresence(roomSecret, getBeacon) {
|
|
|
271
268
|
* @returns {Promise<void>}
|
|
272
269
|
*/
|
|
273
270
|
export async function sendNodeSignalPacket(toNodeHash, packet) {
|
|
274
|
-
|
|
275
|
-
await sendNodeSignal(hash, encryptSignalPacket(nodeRendezvousKey(hash), packet))
|
|
271
|
+
await sendNodeSignal(toNodeHash, encryptSignalPacket(nodeRendezvousKey(toNodeHash), packet))
|
|
276
272
|
}
|
|
277
273
|
|
|
278
274
|
/**
|
|
@@ -282,7 +278,7 @@ export async function sendNodeSignalPacket(toNodeHash, packet) {
|
|
|
282
278
|
* @returns {object | null} 解密 JSON
|
|
283
279
|
*/
|
|
284
280
|
export function decryptNodeSignalPacket(localNodeHash, bytes) {
|
|
285
|
-
return decryptSignalPacket(nodeRendezvousKey(
|
|
281
|
+
return decryptSignalPacket(nodeRendezvousKey(localNodeHash), bytes)
|
|
286
282
|
}
|
|
287
283
|
|
|
288
284
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
|
-
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
5
4
|
import { sha256Hex } from '../../crypto/crypto.mjs'
|
|
6
5
|
import { createLruMap } from '../../utils/lru.mjs'
|
|
7
6
|
|
|
@@ -19,7 +18,7 @@ export const SIGNAL_KEY_CACHE_MAX = 512
|
|
|
19
18
|
* @returns {string} rendezvous 键
|
|
20
19
|
*/
|
|
21
20
|
export function nodeRendezvousKey(nodeHash) {
|
|
22
|
-
return sha256Hex(`${NODE_RENDEZVOUS_DOMAIN}${
|
|
21
|
+
return sha256Hex(`${NODE_RENDEZVOUS_DOMAIN}${nodeHash}`)
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
package/discovery/lan.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import dgram from 'node:dgram'
|
|
3
3
|
|
|
4
4
|
import { base64ToBytes, bytesToBase64 } from '../core/bytes_codec.mjs'
|
|
5
|
-
import { isHex64
|
|
5
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
6
6
|
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
7
7
|
|
|
8
8
|
import { noteAdvertPeerHints } from './advert_peer_hints.mjs'
|
|
@@ -25,8 +25,8 @@ const visibleByHash = new Map()
|
|
|
25
25
|
* @returns {void}
|
|
26
26
|
*/
|
|
27
27
|
export function noteLanVisibleNode(nodeHash, now = Date.now()) {
|
|
28
|
-
const hash =
|
|
29
|
-
if (!
|
|
28
|
+
const hash = isHex64(nodeHash)
|
|
29
|
+
if (!hash) return
|
|
30
30
|
visibleByHash.set(hash, now)
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -61,7 +61,7 @@ export async function acceptLanPresenceAdvert(advertBytes, meta = {}) {
|
|
|
61
61
|
if (!advertBytes?.byteLength) return null
|
|
62
62
|
const ingested = await ingestNetworkAdvert(advertBytes)
|
|
63
63
|
if (!ingested) return null
|
|
64
|
-
const skipHash =
|
|
64
|
+
const skipHash = isHex64(meta.skipNodeHash)
|
|
65
65
|
if (skipHash && ingested.verifiedNodeHash === skipHash) return ingested
|
|
66
66
|
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
67
67
|
noteLanVisibleNode(ingested.verifiedNodeHash)
|
|
@@ -93,9 +93,8 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
93
93
|
let refs = 0
|
|
94
94
|
/** @type {ReturnType<typeof setInterval> | null} */
|
|
95
95
|
let beaconTimer = null
|
|
96
|
-
const seededSelf = normalizeHex64(options.localNodeHash)
|
|
97
96
|
/** @type {string | null} */
|
|
98
|
-
let selfNodeHash = isHex64(
|
|
97
|
+
let selfNodeHash = isHex64(options.localNodeHash)
|
|
99
98
|
/** @type {Set<string>} */
|
|
100
99
|
const joinedAddresses = new Set()
|
|
101
100
|
|
|
@@ -249,8 +248,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
249
248
|
* @returns {Promise<boolean>} 存在 peer hint 时为 true
|
|
250
249
|
*/
|
|
251
250
|
async connectToNode(nodeHash) {
|
|
252
|
-
|
|
253
|
-
return isHex64(hash) && !!getLanPeerHint(hash)
|
|
251
|
+
return !!getLanPeerHint(nodeHash)
|
|
254
252
|
},
|
|
255
253
|
/**
|
|
256
254
|
* @param {() => Promise<{ nodeHash?: string, tcpPort?: number, advertBytes?: Uint8Array, advertBody?: object } | null>} getBeacon 本机 beacon
|
|
@@ -264,7 +262,7 @@ export function createLanDiscoveryProvider(options = {}) {
|
|
|
264
262
|
const send = async () => {
|
|
265
263
|
const body = await getBeacon?.()
|
|
266
264
|
if (!body) return
|
|
267
|
-
if (body.nodeHash) selfNodeHash =
|
|
265
|
+
if (body.nodeHash) selfNodeHash = body.nodeHash
|
|
268
266
|
const advertBytes = body.advertBytes?.byteLength
|
|
269
267
|
? body.advertBytes
|
|
270
268
|
: null
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
1
|
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
3
2
|
import { createTtlMap } from '../utils/ttl_map.mjs'
|
|
4
3
|
|
|
@@ -19,14 +18,13 @@ const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
|
|
|
19
18
|
* @returns {void}
|
|
20
19
|
*/
|
|
21
20
|
export function noteLanPeerHint(nodeHash, endpoint) {
|
|
22
|
-
const hash = normalizeHex64(nodeHash)
|
|
23
21
|
const host = String(endpoint?.host || '')
|
|
24
22
|
const port = normalizeTcpPort(endpoint?.port)
|
|
25
|
-
if (!
|
|
26
|
-
const existing = hints.get(
|
|
23
|
+
if (!nodeHash || !host || !port) return
|
|
24
|
+
const existing = hints.get(nodeHash)?.endpoints ?? []
|
|
27
25
|
const next = existing.filter(item => !(item.host === host && item.port === port))
|
|
28
26
|
next.unshift({ host, port })
|
|
29
|
-
hints.set(
|
|
27
|
+
hints.set(nodeHash, { endpoints: next.slice(0, MAX_ENDPOINTS) })
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/**
|
|
@@ -46,9 +44,8 @@ export function getLanPeerHint(nodeHash, now = Date.now()) {
|
|
|
46
44
|
* @returns {{ host: string, port: number }[]} hint 列表
|
|
47
45
|
*/
|
|
48
46
|
export function listLanPeerHints(nodeHash, now = Date.now()) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return hints.get(hash, now)?.endpoints ?? []
|
|
47
|
+
if (!nodeHash) return []
|
|
48
|
+
return hints.get(nodeHash, now)?.endpoints ?? []
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
/**
|