@steve02081504/fount-p2p 0.0.34 → 0.0.36
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 +1 -1
- package/core/entity_id.mjs +6 -6
- package/core/entity_id_parse.mjs +7 -7
- package/core/hexIds.mjs +9 -19
- package/crypto/checkpoint_sign.mjs +4 -4
- package/dag/canonicalize_row.mjs +4 -7
- package/discovery/adverts.mjs +1 -2
- 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.mjs +19 -20
- package/discovery/peer_clue.mjs +1 -4
- package/federation/entity_key_chain.mjs +10 -9
- package/federation/message_rate_limit.mjs +1 -3
- 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/infra/priority.mjs +1 -2
- package/link/handshake.mjs +22 -23
- 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 +6 -6
- package/mailbox/deliver_or_store.mjs +11 -11
- 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/identity.mjs +6 -6
- package/node/network.mjs +15 -17
- package/node/personal_block.mjs +4 -4
- package/node/reputation_store.mjs +13 -13
- package/node/reputation_sync.mjs +9 -12
- package/package.json +1 -1
- package/reputation/engine.mjs +14 -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/trust_graph/send.mjs +3 -3
|
@@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'
|
|
|
5
5
|
|
|
6
6
|
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
7
7
|
import { hashFromPubKeyHex } from '../core/entity_id.mjs'
|
|
8
|
-
import { isHex64
|
|
8
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* entity 密钥撤销签名的域分隔前缀。
|
|
@@ -28,9 +28,10 @@ export const ENTITY_KEY_REVOKE_DOMAIN = 'fount-entity-key-revoke'
|
|
|
28
28
|
* @returns {EntityKeyHistoryEntry[]} 创世链
|
|
29
29
|
*/
|
|
30
30
|
export function createGenesisKeyHistory(activePubKeyHex, validFrom = Date.now()) {
|
|
31
|
+
if (!activePubKeyHex) return []
|
|
31
32
|
return [{
|
|
32
33
|
generation: 0,
|
|
33
|
-
activePubKeyHex
|
|
34
|
+
activePubKeyHex,
|
|
34
35
|
attestedBy: 'recovery',
|
|
35
36
|
validFrom,
|
|
36
37
|
}]
|
|
@@ -63,8 +64,8 @@ export function isActiveGenerationRevoked(keyHistory, generation) {
|
|
|
63
64
|
* @returns {boolean} sender 是否为未吊销的活跃钥
|
|
64
65
|
*/
|
|
65
66
|
export function isValidActiveSender(keyHistory, senderPubKeyHash) {
|
|
66
|
-
const sender =
|
|
67
|
-
if (!
|
|
67
|
+
const sender = isHex64(senderPubKeyHash)
|
|
68
|
+
if (!sender) return false
|
|
68
69
|
for (const entry of keyHistory || []) {
|
|
69
70
|
if (isActiveGenerationRevoked(keyHistory, entry.generation)) continue
|
|
70
71
|
if (hashFromPubKeyHex(entry.activePubKeyHex) === sender)
|
|
@@ -79,7 +80,7 @@ export function isValidActiveSender(keyHistory, senderPubKeyHash) {
|
|
|
79
80
|
* @returns {boolean} 是否为 recovery 钥签名
|
|
80
81
|
*/
|
|
81
82
|
export function isRecoverySender(recoveryPubKeyHex, senderPubKeyHash) {
|
|
82
|
-
return hashFromPubKeyHex(recoveryPubKeyHex) ===
|
|
83
|
+
return hashFromPubKeyHex(recoveryPubKeyHex) === senderPubKeyHash
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -89,8 +90,8 @@ export function isRecoverySender(recoveryPubKeyHex, senderPubKeyHash) {
|
|
|
89
90
|
*/
|
|
90
91
|
export function reduceEntityKeyRotate(state, event) {
|
|
91
92
|
const generation = Number(event.content?.generation)
|
|
92
|
-
const activePubKeyHex =
|
|
93
|
-
if (!Number.isFinite(generation) || generation < 0 || !
|
|
93
|
+
const activePubKeyHex = isHex64(event.content?.activePubKeyHex || '')
|
|
94
|
+
if (!Number.isFinite(generation) || generation < 0 || !activePubKeyHex)
|
|
94
95
|
return state
|
|
95
96
|
state.entityKeyHistory = state.entityKeyHistory || []
|
|
96
97
|
if (state.entityKeyHistory.some(row => row.generation === generation))
|
|
@@ -111,7 +112,7 @@ export function reduceEntityKeyRotate(state, event) {
|
|
|
111
112
|
*/
|
|
112
113
|
export function reduceEntityKeyRevoke(state, event) {
|
|
113
114
|
const newGeneration = Number(event.content?.newGeneration)
|
|
114
|
-
const activePubKeyHex =
|
|
115
|
+
const activePubKeyHex = event.content?.activePubKeyHex
|
|
115
116
|
const revokeGenerations = (event.content?.revokeGenerations || [])
|
|
116
117
|
.map(Number).filter(Number.isFinite)
|
|
117
118
|
if (!Number.isFinite(newGeneration) || newGeneration < 0 || !isHex64(activePubKeyHex))
|
|
@@ -163,7 +164,7 @@ export function entityKeyRevokeSignBytes(revokeBody) {
|
|
|
163
164
|
const body = {
|
|
164
165
|
revokeGenerations: (revokeBody.revokeGenerations || []).map(Number),
|
|
165
166
|
newGeneration: Number(revokeBody.newGeneration),
|
|
166
|
-
activePubKeyHex:
|
|
167
|
+
activePubKeyHex: revokeBody.activePubKeyHex || '',
|
|
167
168
|
entityHash: String(revokeBody.entityHash || ''),
|
|
168
169
|
}
|
|
169
170
|
return Buffer.from(`${ENTITY_KEY_REVOKE_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 群级消息限速纯函数(无 I/O,供单测与 governance 复用)。
|
|
3
3
|
*/
|
|
4
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* @param {object} event DAG 事件
|
|
8
6
|
* @returns {string} 限速实体键
|
|
@@ -10,7 +8,7 @@ import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
|
10
8
|
export function messageRateEntityKey(event) {
|
|
11
9
|
const charId = String(event?.charId || '')
|
|
12
10
|
if (charId) return `char:${charId}`
|
|
13
|
-
return
|
|
11
|
+
return event?.sender || ''
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
@@ -40,8 +40,8 @@ const TRANSFER_TYPES = new Set(['public', 'file-master-key-wrap', 'vault-wrap',
|
|
|
40
40
|
*/
|
|
41
41
|
export function normalizeFileManifest(input) {
|
|
42
42
|
if (!input || typeof input !== 'object') return null
|
|
43
|
-
const ownerEntityHash =
|
|
44
|
-
if (!
|
|
43
|
+
const ownerEntityHash = isEntityHash128(input.ownerEntityHash)
|
|
44
|
+
if (!ownerEntityHash) return null
|
|
45
45
|
const logicalPath = String(input.logicalPath || '').replace(/^\/+/, '').replace(/\\/g, '/')
|
|
46
46
|
if (!logicalPath) return null
|
|
47
47
|
const ceMode = String(input.ceMode || 'convergent')
|
|
@@ -3,7 +3,7 @@ import { Buffer } from 'node:buffer'
|
|
|
3
3
|
import { canonicalStringify } from '../../core/canonical_json.mjs'
|
|
4
4
|
import { hashFromPubKeyHex, parseEntityHash } from '../../core/entity_id.mjs'
|
|
5
5
|
import { assertSafeEvfsLogicalPath } from '../../core/evfs_logical_path.mjs'
|
|
6
|
-
import { isHex64, isSignatureHex128
|
|
6
|
+
import { isHex64, isSignatureHex128 } from '../../core/hexIds.mjs'
|
|
7
7
|
import { sign, verify } from '../../crypto/crypto.mjs'
|
|
8
8
|
import { putFileManifest, saveFileManifest } from '../evfs.mjs'
|
|
9
9
|
|
|
@@ -39,7 +39,7 @@ export function publicManifestSignBytes(fields) {
|
|
|
39
39
|
* @returns {Promise<import('./normalize.mjs').FileManifest>} 带 publicSig 的清单
|
|
40
40
|
*/
|
|
41
41
|
export async function attachPublicManifestSig(manifest, publishedAt, entitySecretKey, entityPubKeyHex) {
|
|
42
|
-
const pubKeyHex =
|
|
42
|
+
const pubKeyHex = entityPubKeyHex
|
|
43
43
|
const message = publicManifestSignBytes({
|
|
44
44
|
ownerEntityHash: manifest.ownerEntityHash,
|
|
45
45
|
logicalPath: manifest.logicalPath,
|
|
@@ -73,9 +73,9 @@ export async function verifySignedPublicManifest(input) {
|
|
|
73
73
|
const publicSig = input?.meta?.publicSig
|
|
74
74
|
if (!publicSig || typeof publicSig !== 'object') return null
|
|
75
75
|
const publishedAt = Number(publicSig.publishedAt) || 0
|
|
76
|
-
const pubKeyHex =
|
|
77
|
-
const sigHex =
|
|
78
|
-
if (!
|
|
76
|
+
const pubKeyHex = isHex64(publicSig.pubKeyHex)
|
|
77
|
+
const sigHex = isSignatureHex128(publicSig.sigHex)
|
|
78
|
+
if (!pubKeyHex || !sigHex || publishedAt <= 0) return null
|
|
79
79
|
|
|
80
80
|
const parsed = parseEntityHash(manifest.ownerEntityHash)
|
|
81
81
|
if (!parsed) return null
|
package/governance/branch.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
1
|
import { sortedPrevEventIds } from '../dag/index.mjs'
|
|
3
2
|
import { getGovernanceAuthzTypes } from '../registries/event_type.mjs'
|
|
4
3
|
|
|
@@ -105,7 +104,7 @@ function governanceAuthzScoreForTip(tipId, byId, reputationBySender) {
|
|
|
105
104
|
for (const eventId of ancestorClosureFromTip(tipId, byId)) {
|
|
106
105
|
const event = byId.get(eventId)
|
|
107
106
|
if (!event || !getGovernanceAuthzTypes().has(event.type)) continue
|
|
108
|
-
const sender =
|
|
107
|
+
const sender = event.sender
|
|
109
108
|
if (sender) score += Number(reputationBySender[sender] ?? 0)
|
|
110
109
|
}
|
|
111
110
|
return score
|
package/governance/join_pow.mjs
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { createHash } from 'node:crypto'
|
|
5
5
|
|
|
6
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
7
|
-
|
|
8
6
|
import admissionTunables from './tunables.json' with { type: 'json' }
|
|
9
7
|
|
|
10
8
|
/** 默认 epoch 窗口(1 小时) */
|
|
@@ -105,8 +103,8 @@ export function verifyJoinPow(powSolution, options) {
|
|
|
105
103
|
const anchorRef = String(powSolution.anchorRef ?? '')
|
|
106
104
|
const { nonce } = powSolution
|
|
107
105
|
const epoch = Number(powSolution.epoch)
|
|
108
|
-
const joinerNodeHash =
|
|
109
|
-
const senderNodeHash =
|
|
106
|
+
const joinerNodeHash = powSolution.joinerNodeHash ?? options.senderNodeHash
|
|
107
|
+
const senderNodeHash = options.senderNodeHash
|
|
110
108
|
|
|
111
109
|
if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
|
|
112
110
|
if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Buffer } from 'node:buffer'
|
|
5
5
|
|
|
6
6
|
import { canonicalStringify } from '../core/canonical_json.mjs'
|
|
7
|
-
import { isHex64, isSignatureHex128
|
|
7
|
+
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
8
8
|
import { pubKeyHash, verify } from '../crypto/crypto.mjs'
|
|
9
9
|
|
|
10
10
|
const BALLOT_DOMAIN = 'fount-owner-succession'
|
|
@@ -15,7 +15,7 @@ const BALLOT_DOMAIN = 'fount-owner-succession'
|
|
|
15
15
|
*/
|
|
16
16
|
export function ownerSuccessionBallotSignBytes(ballot) {
|
|
17
17
|
const body = {
|
|
18
|
-
proposedOwnerPubKeyHash:
|
|
18
|
+
proposedOwnerPubKeyHash: ballot.proposedOwnerPubKeyHash,
|
|
19
19
|
groupId: ballot.groupId,
|
|
20
20
|
ballotId: ballot.ballotId,
|
|
21
21
|
}
|
|
@@ -42,9 +42,9 @@ export async function verifyOwnerSuccessionThreshold(ballot, adminPubKeyHashes,
|
|
|
42
42
|
let valid = 0
|
|
43
43
|
|
|
44
44
|
for (const entry of ballot.adminSignatures || []) {
|
|
45
|
-
const pubKeyHex =
|
|
46
|
-
const signatureHex = entry.signature
|
|
47
|
-
if (!
|
|
45
|
+
const pubKeyHex = isHex64(entry.pubKeyHex)
|
|
46
|
+
const signatureHex = isSignatureHex128(entry.signature)
|
|
47
|
+
if (!pubKeyHex || !signatureHex) continue
|
|
48
48
|
|
|
49
49
|
const hash = pubKeyHash(Buffer.from(pubKeyHex, 'hex'))
|
|
50
50
|
if (!admins.has(hash) || seen.has(hash)) continue
|
package/infra/priority.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
1
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
3
2
|
import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
|
|
4
3
|
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
@@ -38,7 +37,7 @@ export function applyPriorityToRegistry() {
|
|
|
38
37
|
registry.setPriorityWeightFunction(nodeHash => {
|
|
39
38
|
const score = pickNodeScoreFromReputation(
|
|
40
39
|
loadReputation(),
|
|
41
|
-
|
|
40
|
+
nodeHash,
|
|
42
41
|
)
|
|
43
42
|
return Math.floor(score * PRIORITY_BOOST)
|
|
44
43
|
})
|
package/link/handshake.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
|
-
import { isHex64, isSignatureHex128
|
|
4
|
+
import { isHex64, isSignatureHex128 } from '../core/hexIds.mjs'
|
|
5
5
|
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
6
6
|
import { keyPairFromSeed, pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
7
7
|
import { normalizeLanHosts } from '../discovery/lan_interfaces.mjs'
|
|
@@ -22,8 +22,7 @@ export const LINK_HANDSHAKE_DOMAIN = 'fount-link'
|
|
|
22
22
|
export function normalizeLinkBinding(value) {
|
|
23
23
|
const dtls = normalizeDtlsFingerprint(value)
|
|
24
24
|
if (dtls) return dtls
|
|
25
|
-
|
|
26
|
-
return isHex64(hex) ? hex : null
|
|
25
|
+
return isHex64(value)
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
/**
|
|
@@ -34,9 +33,9 @@ export function normalizeLinkBinding(value) {
|
|
|
34
33
|
* @returns {Uint8Array} 待签名消息字节
|
|
35
34
|
*/
|
|
36
35
|
export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
|
|
37
|
-
const nonce =
|
|
36
|
+
const nonce = peerNonce
|
|
38
37
|
const binding = normalizeLinkBinding(localBinding)
|
|
39
|
-
const nodeHash =
|
|
38
|
+
const nodeHash = localNodeHash
|
|
40
39
|
if (!/^[\da-f]{64}$/u.test(nonce))
|
|
41
40
|
throw new Error('p2p: auth nonce must be 64 hex characters')
|
|
42
41
|
if (!binding)
|
|
@@ -57,10 +56,10 @@ export function buildHello(options = {}) {
|
|
|
57
56
|
const derived = keyPairFromSeed(Buffer.from(ensureNodeSeed(), 'hex'))
|
|
58
57
|
publicKey = derived.publicKey
|
|
59
58
|
}
|
|
60
|
-
const nodeHash =
|
|
61
|
-
const nodePubKey =
|
|
62
|
-
const nonce =
|
|
63
|
-
if (!
|
|
59
|
+
const nodeHash = isHex64(options.nodeHash || getNodeHash())
|
|
60
|
+
const nodePubKey = isHex64(options.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
61
|
+
const nonce = isHex64(options.nonce || randomBytes(32).toString('hex'))
|
|
62
|
+
if (!nodeHash || !nodePubKey || !nonce)
|
|
64
63
|
throw new Error('p2p: invalid hello fields')
|
|
65
64
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
66
65
|
throw new Error('p2p: hello nodePubKey does not match nodeHash')
|
|
@@ -79,7 +78,7 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
|
|
|
79
78
|
? Buffer.from(options.secretKey)
|
|
80
79
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
81
80
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
82
|
-
const nodeHash =
|
|
81
|
+
const nodeHash = options.nodeHash || pubKeyHash(publicKey)
|
|
83
82
|
if (nodeHash !== pubKeyHash(publicKey))
|
|
84
83
|
throw new Error('p2p: auth nodeHash does not match secretKey')
|
|
85
84
|
const message = buildAuthMessage(peerNonce, localBinding, nodeHash)
|
|
@@ -93,10 +92,10 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
|
|
|
93
92
|
* @returns {{ nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
|
|
94
93
|
*/
|
|
95
94
|
export function parseHello(hello) {
|
|
96
|
-
const nodeHash =
|
|
97
|
-
const nodePubKey =
|
|
98
|
-
const nonce =
|
|
99
|
-
if (!
|
|
95
|
+
const nodeHash = isHex64(hello?.nodeHash)
|
|
96
|
+
const nodePubKey = isHex64(hello?.nodePubKey)
|
|
97
|
+
const nonce = isHex64(hello?.nonce)
|
|
98
|
+
if (!nodeHash || !nodePubKey || !nonce) return null
|
|
100
99
|
try {
|
|
101
100
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash) return null
|
|
102
101
|
}
|
|
@@ -117,11 +116,11 @@ export function parseHello(hello) {
|
|
|
117
116
|
export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
118
117
|
const parsedHello = parseHello(hello)
|
|
119
118
|
if (!parsedHello) return null
|
|
120
|
-
const signatureHex =
|
|
119
|
+
const signatureHex = isSignatureHex128(auth?.sig)
|
|
121
120
|
const binding = normalizeLinkBinding(remoteBinding)
|
|
122
|
-
if (!
|
|
123
|
-
const normalizedNonce =
|
|
124
|
-
if (!
|
|
121
|
+
if (!signatureHex || !binding) return null
|
|
122
|
+
const normalizedNonce = isHex64(expectedNonce)
|
|
123
|
+
if (!normalizedNonce) return null
|
|
125
124
|
const message = buildAuthMessage(normalizedNonce, binding, parsedHello.nodeHash)
|
|
126
125
|
const ok = await verify(
|
|
127
126
|
Buffer.from(signatureHex, 'hex'),
|
|
@@ -141,7 +140,7 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
|
141
140
|
* @returns {Uint8Array} 待签名消息字节
|
|
142
141
|
*/
|
|
143
142
|
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null) {
|
|
144
|
-
const base = `fount-advert\0${rendezvousKey}\0${ts}\0${
|
|
143
|
+
const base = `fount-advert\0${rendezvousKey}\0${ts}\0${nodeHash}`
|
|
145
144
|
const port = normalizeTcpPort(tcpPort)
|
|
146
145
|
let message = port ? `${base}\0${port}` : base
|
|
147
146
|
const hosts = normalizeLanHosts(lanHosts)
|
|
@@ -161,8 +160,8 @@ export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options
|
|
|
161
160
|
? Buffer.from(options.secretKey)
|
|
162
161
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
163
162
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
164
|
-
const nodeHash =
|
|
165
|
-
const nodePubKey =
|
|
163
|
+
const nodeHash = options?.nodeHash || pubKeyHash(publicKey)
|
|
164
|
+
const nodePubKey = options?.nodePubKey || Buffer.from(publicKey).toString('hex')
|
|
166
165
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
167
166
|
throw new Error('p2p: advert nodePubKey does not match nodeHash')
|
|
168
167
|
const tcpPort = normalizeTcpPort(options?.tcpPort)
|
|
@@ -194,8 +193,8 @@ export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now()
|
|
|
194
193
|
const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
|
|
195
194
|
if (!parsedHello) return null
|
|
196
195
|
const ts = Number(advert?.ts)
|
|
197
|
-
const sig =
|
|
198
|
-
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !
|
|
196
|
+
const sig = isSignatureHex128(advert?.sig)
|
|
197
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !sig) return null
|
|
199
198
|
const hasTcpPortField = !!advert?.tcpPort
|
|
200
199
|
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
201
200
|
if (hasTcpPortField && !tcpPort) return null
|
package/link/pipe.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { toBytes } from '../core/bytes_codec.mjs'
|
|
2
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
3
2
|
import { ms } from '../utils/duration.mjs'
|
|
4
3
|
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
5
4
|
import { createLruMap } from '../utils/lru.mjs'
|
|
@@ -92,7 +91,7 @@ export function createLinkPipe(options) {
|
|
|
92
91
|
const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
|
|
93
92
|
const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
|
|
94
93
|
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
95
|
-
const targetNodeHash =
|
|
94
|
+
const targetNodeHash = options.nodeHash || ''
|
|
96
95
|
let closed = false
|
|
97
96
|
let ready = false
|
|
98
97
|
let closeReason = 'closed'
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
|
-
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
5
4
|
import { getBtPeerHint } from '../../discovery/bt/peer_hints.mjs'
|
|
6
5
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from '../../discovery/bt/runtime.mjs'
|
|
7
6
|
import { asLinkHandle } from '../pipe.mjs'
|
|
@@ -55,7 +54,7 @@ async function openGattPipe(options) {
|
|
|
55
54
|
|
|
56
55
|
if (options.initiator)
|
|
57
56
|
await Promise.resolve(options.write(Buffer.from(
|
|
58
|
-
buildLinkOpen(
|
|
57
|
+
buildLinkOpen(options.linkId, options.localIdentity?.nodeHash),
|
|
59
58
|
'utf8',
|
|
60
59
|
)))
|
|
61
60
|
|
|
@@ -69,7 +68,7 @@ async function openGattPipe(options) {
|
|
|
69
68
|
* @returns {Promise<import('./index.mjs').LinkHandle>} 已就绪的 link
|
|
70
69
|
*/
|
|
71
70
|
async function dialBleGatt(options) {
|
|
72
|
-
const remoteNodeHash =
|
|
71
|
+
const remoteNodeHash = options.nodeHash
|
|
73
72
|
const hint = getBtPeerHint(remoteNodeHash)
|
|
74
73
|
if (!hint) throw new Error('p2p: ble_gatt no peer hint')
|
|
75
74
|
const noble = await loadNoble()
|
|
@@ -2,7 +2,6 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
import net from 'node:net'
|
|
4
4
|
|
|
5
|
-
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
5
|
import { getLanPeerHint, listLanPeerHints } from '../../discovery/lan_peer_hints.mjs'
|
|
7
6
|
import { asLinkHandle } from '../pipe.mjs'
|
|
8
7
|
|
|
@@ -186,7 +185,7 @@ async function openTcpPipe(options) {
|
|
|
186
185
|
pipe.handleInbound(payload)
|
|
187
186
|
|
|
188
187
|
if (options.initiator)
|
|
189
|
-
codec.write(buildLinkOpen(
|
|
188
|
+
codec.write(buildLinkOpen(options.linkId, options.localIdentity?.nodeHash))
|
|
190
189
|
|
|
191
190
|
await pipe.startHandshake()
|
|
192
191
|
return asLinkHandle(pipe)
|
|
@@ -198,7 +197,7 @@ async function openTcpPipe(options) {
|
|
|
198
197
|
* @returns {Promise<import('./index.mjs').LinkHandle>} 已就绪的 link
|
|
199
198
|
*/
|
|
200
199
|
async function dialLanTcp(options) {
|
|
201
|
-
const remoteNodeHash =
|
|
200
|
+
const remoteNodeHash = options.nodeHash
|
|
202
201
|
const hints = listLanPeerHints(remoteNodeHash)
|
|
203
202
|
if (!hints.length) throw new Error('p2p: lan_tcp no peer hint')
|
|
204
203
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isHex64 } from '../../core/hexIds.mjs'
|
|
4
4
|
import { createLinkPipe } from '../pipe.mjs'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,7 +25,7 @@ export function parseLinkOpen(raw) {
|
|
|
25
25
|
}
|
|
26
26
|
catch { return null }
|
|
27
27
|
if (parsed?.type !== 'link-open' || !parsed.linkId) return null
|
|
28
|
-
return { linkId: String(parsed.linkId), from:
|
|
28
|
+
return { linkId: String(parsed.linkId), from: isHex64(parsed.from) }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -34,7 +34,7 @@ export function parseLinkOpen(raw) {
|
|
|
34
34
|
* @returns {ReturnType<typeof createLinkPipe>} link pipe
|
|
35
35
|
*/
|
|
36
36
|
export function createLinkIdBoundPipe(options) {
|
|
37
|
-
const linkId =
|
|
37
|
+
const linkId = options.linkId
|
|
38
38
|
if (!linkId) throw new Error(`p2p: ${options.providerId || 'link'} linkId required`)
|
|
39
39
|
return createLinkPipe({
|
|
40
40
|
...options,
|
package/link/providers/nostr.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { base64ToBytes, bytesToBase64 } from '../../core/bytes_codec.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { isHex64 } from '../../core/hexIds.mjs'
|
|
6
6
|
import { getDiscoveryProvider, sendNodeSignalPacket } from '../../discovery/index.mjs'
|
|
7
7
|
import { NOSTR_SIGNAL_KIND, resolveNostrRelayUrls } from '../../discovery/nostr.mjs'
|
|
8
8
|
import { ms } from '../../utils/duration.mjs'
|
|
@@ -317,9 +317,9 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
317
317
|
*/
|
|
318
318
|
async function deliverPacket(packet) {
|
|
319
319
|
if (packet?.type !== 'link') return
|
|
320
|
-
const linkId =
|
|
321
|
-
const from =
|
|
322
|
-
if (!linkId || !from) return
|
|
320
|
+
const linkId = packet.linkId
|
|
321
|
+
const from = packet.from
|
|
322
|
+
if (!isHex64(linkId) || !isHex64(from)) return
|
|
323
323
|
if (localIdentity?.nodeHash && from === localIdentity.nodeHash) return
|
|
324
324
|
|
|
325
325
|
const op = String(packet.op || '')
|
|
@@ -375,8 +375,8 @@ export function createNostrLinkProvider(options = {}) {
|
|
|
375
375
|
*/
|
|
376
376
|
async dial(dialOptions) {
|
|
377
377
|
if (!isAvailable()) return null
|
|
378
|
-
const remoteNodeHash =
|
|
379
|
-
if (!remoteNodeHash) return null
|
|
378
|
+
const remoteNodeHash = dialOptions.nodeHash
|
|
379
|
+
if (!isHex64(remoteNodeHash)) return null
|
|
380
380
|
localIdentity = dialOptions.localIdentity || localIdentity
|
|
381
381
|
if (!localIdentity?.nodeHash) throw new Error('p2p: nostr dial requires localIdentity')
|
|
382
382
|
await refreshPayloadCap(resolveRelayUrls())
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
3
|
import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
|
|
4
4
|
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
26
|
if (!peerId) return null
|
|
27
27
|
const entry = activeLinkRoster().find(row => row.peerId === peerId)
|
|
28
|
-
const remote =
|
|
28
|
+
const remote = entry?.remoteNodeHash
|
|
29
29
|
return remote || null
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -62,8 +62,8 @@ async function resolveRouting(username) {
|
|
|
62
62
|
*/
|
|
63
63
|
export async function deliverOrStoreMailboxPut(username, options) {
|
|
64
64
|
const routing = await resolveRouting(username)
|
|
65
|
-
const toPubKeyHash =
|
|
66
|
-
if (!toPubKeyHash) return { stored: false, delivered: false, relayed: 0 }
|
|
65
|
+
const toPubKeyHash = options.toPubKeyHash
|
|
66
|
+
if (!isHex64(toPubKeyHash)) return { stored: false, delivered: false, relayed: 0 }
|
|
67
67
|
const hop = normalizeMailboxHop(options.hop)
|
|
68
68
|
if (hop >= routing.maxHop) return { stored: false, delivered: false, relayed: 0 }
|
|
69
69
|
const tier = mailboxTierFromHop(hop)
|
|
@@ -76,8 +76,8 @@ export async function deliverOrStoreMailboxPut(username, options) {
|
|
|
76
76
|
fromNodeHash: options.record?.fromNodeHash || nodeHash,
|
|
77
77
|
}
|
|
78
78
|
const stored = await storeMailboxRecord(record)
|
|
79
|
-
const toNodeHash =
|
|
80
|
-
const delivered = toNodeHash && isMailboxRecordWithinSizeLimit(record)
|
|
79
|
+
const toNodeHash = options.toNodeHash
|
|
80
|
+
const delivered = isHex64(toNodeHash) && isMailboxRecordWithinSizeLimit(record)
|
|
81
81
|
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
82
82
|
: false
|
|
83
83
|
|
|
@@ -114,8 +114,8 @@ export async function publishMailboxRecord(username, toPubKeyHash, record, toNod
|
|
|
114
114
|
export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
115
115
|
const { record } = put
|
|
116
116
|
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
117
|
-
const fromNode =
|
|
118
|
-
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
117
|
+
const fromNode = put.nodeHash
|
|
118
|
+
if (!isHex64(fromNode) || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
119
119
|
const username = wireContext.replicaUsername || ''
|
|
120
120
|
if (!username) return
|
|
121
121
|
if (peerId) {
|
|
@@ -143,8 +143,8 @@ export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
|
143
143
|
*/
|
|
144
144
|
export async function respondMailboxWant(want, sendGive, peerId) {
|
|
145
145
|
const { getMailboxRecords, takeMailboxForRecipient } = await import('./store.mjs')
|
|
146
|
-
const recipient =
|
|
147
|
-
if (!recipient) return
|
|
146
|
+
const recipient = want.toPubKeyHash
|
|
147
|
+
if (!isHex64(recipient)) return
|
|
148
148
|
const ids = want.ids || []
|
|
149
149
|
const rows = (ids.length
|
|
150
150
|
? await getMailboxRecords(ids)
|
|
@@ -179,7 +179,7 @@ export async function ingestMailboxGive(wireContext, give) {
|
|
|
179
179
|
export async function requestMailboxFromNetwork(username, toPubKeyHash) {
|
|
180
180
|
const routing = await resolveRouting(username)
|
|
181
181
|
const { listMailboxIdsForRecipient } = await import('./store.mjs')
|
|
182
|
-
const recipient =
|
|
182
|
+
const recipient = toPubKeyHash
|
|
183
183
|
if (!recipient) return
|
|
184
184
|
await deliverToUserRoomPeers(username, 'mailbox_want', {
|
|
185
185
|
toPubKeyHash: recipient,
|
package/mailbox/prune.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mailbox 公平淘汰(纯函数,供 store 与单测复用)。
|
|
3
3
|
*/
|
|
4
|
-
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
4
|
|
|
6
5
|
/** 单收件人×发送者桶:条数上限 */
|
|
7
6
|
export const MAX_BUCKET_ENTRIES = 10
|
|
@@ -31,9 +30,9 @@ export function mailboxRecordBytes(record) {
|
|
|
31
30
|
* @returns {string} 公平淘汰桶键
|
|
32
31
|
*/
|
|
33
32
|
export function mailboxBucketKey(record) {
|
|
34
|
-
const to =
|
|
35
|
-
const from =
|
|
36
|
-
||
|
|
33
|
+
const to = record.toPubKeyHash || ''
|
|
34
|
+
const from = record.fromNodeHash
|
|
35
|
+
|| record.envelope?.sender
|
|
37
36
|
|| 'unknown'
|
|
38
37
|
return `${to}\0${from}`
|
|
39
38
|
}
|
package/mailbox/rate.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mailbox 入站 put 限速(按来源节点,节点级单例)。
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
5
|
|
|
6
6
|
const DEFAULT_WINDOW_MS = 60_000
|
|
7
7
|
const DEFAULT_MAX_PUTS = 20
|
|
@@ -62,7 +62,8 @@ function evictLruIfNeeded(key) {
|
|
|
62
62
|
*/
|
|
63
63
|
export function takeIncomingMailboxPutSlot(fromNodeHash, limits) {
|
|
64
64
|
const { windowMs, maxPuts } = resolveMailboxRateLimits(limits)
|
|
65
|
-
const key =
|
|
65
|
+
const key = fromNodeHash
|
|
66
|
+
if (!isHex64(key)) return false
|
|
66
67
|
const now = Date.now()
|
|
67
68
|
if (inboundByKey.size >= MAX_KEYS) sweepExpiredEntries(now)
|
|
68
69
|
evictLruIfNeeded(key)
|
package/mailbox/store.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile } from 'node:fs/promises'
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { isHex64
|
|
4
|
+
import { isHex64 } from '../core/hexIds.mjs'
|
|
5
5
|
import { jsonlMutexKey, writeJsonl } from '../dag/storage.mjs'
|
|
6
6
|
import { mailboxStorePath } from '../node/user_paths.mjs'
|
|
7
7
|
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
@@ -150,7 +150,7 @@ export function mailboxEnvelopeId(envelope) {
|
|
|
150
150
|
*/
|
|
151
151
|
export async function storeMailboxRecord(record) {
|
|
152
152
|
if (!isMailboxRecordWithinSizeLimit(record)) return false
|
|
153
|
-
const toPubKeyHash =
|
|
153
|
+
const toPubKeyHash = record.toPubKeyHash
|
|
154
154
|
if (!isHex64(toPubKeyHash)) return false
|
|
155
155
|
const hop = normalizeMailboxHop(record.hop)
|
|
156
156
|
const { tier } = record
|
|
@@ -178,7 +178,7 @@ export async function storeMailboxRecord(record) {
|
|
|
178
178
|
envelope: record.envelope,
|
|
179
179
|
storedAt: Date.now(),
|
|
180
180
|
expiresAt: Date.now() + ttlMs,
|
|
181
|
-
fromNodeHash:
|
|
181
|
+
fromNodeHash: record.fromNodeHash,
|
|
182
182
|
hop,
|
|
183
183
|
tier,
|
|
184
184
|
importance: Number.isFinite(Number(record.importance)) ? Number(record.importance) : undefined,
|
|
@@ -193,9 +193,8 @@ export async function storeMailboxRecord(record) {
|
|
|
193
193
|
* @returns {Promise<string[]>} record id 列表
|
|
194
194
|
*/
|
|
195
195
|
export async function listMailboxIdsForRecipient(toPubKeyHash) {
|
|
196
|
-
const recipient = normalizeHex64(toPubKeyHash)
|
|
197
196
|
return (await readAll())
|
|
198
|
-
.filter(record => record.toPubKeyHash ===
|
|
197
|
+
.filter(record => record.toPubKeyHash === toPubKeyHash)
|
|
199
198
|
.map(record => record.id)
|
|
200
199
|
}
|
|
201
200
|
|
|
@@ -224,8 +223,7 @@ export async function deleteMailboxRecords(ids) {
|
|
|
224
223
|
* @returns {Promise<MailboxRecord[]>} 待发记录(不删除)
|
|
225
224
|
*/
|
|
226
225
|
export async function takeMailboxForRecipient(toPubKeyHash) {
|
|
227
|
-
|
|
228
|
-
return (await readAll()).filter(record => record.toPubKeyHash === recipient)
|
|
226
|
+
return (await readAll()).filter(record => record.toPubKeyHash === toPubKeyHash)
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
/**
|
|
@@ -233,9 +231,8 @@ export async function takeMailboxForRecipient(toPubKeyHash) {
|
|
|
233
231
|
* @returns {Promise<number>} 未过期条数
|
|
234
232
|
*/
|
|
235
233
|
export async function countMailboxPendingForRecipient(toPubKeyHash) {
|
|
236
|
-
const recipient = normalizeHex64(toPubKeyHash)
|
|
237
234
|
const now = Date.now()
|
|
238
|
-
return (await readAll()).filter(record => record.toPubKeyHash ===
|
|
235
|
+
return (await readAll()).filter(record => record.toPubKeyHash === toPubKeyHash && record.expiresAt > now).length
|
|
239
236
|
}
|
|
240
237
|
|
|
241
238
|
/**
|