@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
|
@@ -10,7 +10,7 @@ const dagPlaintextReadersByOwner = new Map()
|
|
|
10
10
|
* @returns {void}
|
|
11
11
|
*/
|
|
12
12
|
export function registerTransferKeyDependencies(ownerId, dependencies) {
|
|
13
|
-
const key =
|
|
13
|
+
const key = ownerId
|
|
14
14
|
const prev = transferDependenciesByOwner.get(key) || {}
|
|
15
15
|
transferDependenciesByOwner.set(key, { ...prev, ...dependencies })
|
|
16
16
|
}
|
|
@@ -21,7 +21,7 @@ export function registerTransferKeyDependencies(ownerId, dependencies) {
|
|
|
21
21
|
* @returns {void}
|
|
22
22
|
*/
|
|
23
23
|
export function registerDagManifestPlaintextReader(ownerId, reader) {
|
|
24
|
-
dagPlaintextReadersByOwner.set(
|
|
24
|
+
dagPlaintextReadersByOwner.set(ownerId, reader)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** @type {Array<{ ownerId: string, match: (manifest: import('./manifest/normalize.mjs').FileManifest) => boolean }>} */
|
|
@@ -33,7 +33,7 @@ const manifestOwnerMatchers = []
|
|
|
33
33
|
* @returns {void}
|
|
34
34
|
*/
|
|
35
35
|
export function registerManifestOwnerMatcher(ownerId, match) {
|
|
36
|
-
manifestOwnerMatchers.push({ ownerId
|
|
36
|
+
manifestOwnerMatchers.push({ ownerId, match })
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -41,7 +41,7 @@ export function registerManifestOwnerMatcher(ownerId, match) {
|
|
|
41
41
|
* @returns {void}
|
|
42
42
|
*/
|
|
43
43
|
export function unregisterManifestOwnerMatchers(ownerId) {
|
|
44
|
-
const id =
|
|
44
|
+
const id = ownerId
|
|
45
45
|
for (let i = manifestOwnerMatchers.length - 1; i >= 0; i--)
|
|
46
46
|
if (manifestOwnerMatchers[i].ownerId === id) manifestOwnerMatchers.splice(i, 1)
|
|
47
47
|
}
|
|
@@ -51,7 +51,7 @@ export function unregisterManifestOwnerMatchers(ownerId) {
|
|
|
51
51
|
* @returns {void}
|
|
52
52
|
*/
|
|
53
53
|
export function unregisterTransferKeyDependencies(ownerId) {
|
|
54
|
-
const key =
|
|
54
|
+
const key = ownerId
|
|
55
55
|
transferDependenciesByOwner.delete(key)
|
|
56
56
|
dagPlaintextReadersByOwner.delete(key)
|
|
57
57
|
unregisterManifestOwnerMatchers(key)
|
|
@@ -75,7 +75,7 @@ export function resolveManifestTransferOwnerId(manifest) {
|
|
|
75
75
|
*/
|
|
76
76
|
export function resolveTransferKeyDependencies(ownerId, manifest) {
|
|
77
77
|
const resolved = ownerId || (manifest ? resolveManifestTransferOwnerId(manifest) : null)
|
|
78
|
-
if (resolved) return transferDependenciesByOwner.get(
|
|
78
|
+
if (resolved) return transferDependenciesByOwner.get(resolved) || {}
|
|
79
79
|
return {}
|
|
80
80
|
}
|
|
81
81
|
|
package/governance/branch.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
1
2
|
import { sortedPrevEventIds } from '../dag/index.mjs'
|
|
2
3
|
import { getGovernanceAuthzTypes } from '../registries/event_type.mjs'
|
|
3
4
|
|
|
@@ -104,7 +105,7 @@ function governanceAuthzScoreForTip(tipId, byId, reputationBySender) {
|
|
|
104
105
|
for (const eventId of ancestorClosureFromTip(tipId, byId)) {
|
|
105
106
|
const event = byId.get(eventId)
|
|
106
107
|
if (!event || !getGovernanceAuthzTypes().has(event.type)) continue
|
|
107
|
-
const sender =
|
|
108
|
+
const sender = normalizeHex64(event.sender)
|
|
108
109
|
if (sender) score += Number(reputationBySender[sender] ?? 0)
|
|
109
110
|
}
|
|
110
111
|
return score
|
package/governance/join_pow.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { createHash } from 'node:crypto'
|
|
5
5
|
|
|
6
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
7
|
+
|
|
6
8
|
import admissionTunables from './tunables.json' with { type: 'json' }
|
|
7
9
|
|
|
8
10
|
/** 默认 epoch 窗口(1 小时) */
|
|
@@ -21,7 +23,7 @@ export const JOIN_POW_DEFAULT_EPOCH_SKEW = 1
|
|
|
21
23
|
* @returns {string} SHA-256 十六进制
|
|
22
24
|
*/
|
|
23
25
|
export function computeJoinPowHash({ groupId, anchorRef, joinerNodeHash, epoch, nonce }) {
|
|
24
|
-
const preimage = `${
|
|
26
|
+
const preimage = `${groupId}:${anchorRef}:${joinerNodeHash}:${String(epoch)}:${String(nonce)}`
|
|
25
27
|
return createHash('sha256').update(preimage, 'utf8').digest('hex')
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -100,16 +102,16 @@ export function verifyJoinPow(powSolution, options) {
|
|
|
100
102
|
if (floorBits <= 0) return { ok: true, achievedBits: 0 }
|
|
101
103
|
if (!powSolution || typeof powSolution !== 'object') return { ok: false, achievedBits: 0 }
|
|
102
104
|
|
|
103
|
-
const anchorRef = String(powSolution.anchorRef ?? '')
|
|
105
|
+
const anchorRef = String(powSolution.anchorRef ?? '')
|
|
104
106
|
const { nonce } = powSolution
|
|
105
107
|
const epoch = Number(powSolution.epoch)
|
|
106
|
-
const joinerNodeHash =
|
|
107
|
-
const senderNodeHash =
|
|
108
|
+
const joinerNodeHash = normalizeHex64(powSolution.joinerNodeHash ?? options.senderNodeHash)
|
|
109
|
+
const senderNodeHash = normalizeHex64(options.senderNodeHash)
|
|
108
110
|
|
|
109
111
|
if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
|
|
110
112
|
if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
|
|
111
113
|
|
|
112
|
-
const anchors = (options.knownAnchors ?? []).
|
|
114
|
+
const anchors = (options.knownAnchors ?? []).filter(Boolean)
|
|
113
115
|
if (!anchors.length || !anchors.includes(anchorRef)) return { ok: false, achievedBits: 0 }
|
|
114
116
|
|
|
115
117
|
const epochMs = Math.max(60_000, Number(options.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
|
|
@@ -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 } from '../core/hexIds.mjs'
|
|
7
|
+
import { isHex64, isSignatureHex128, normalizeHex64 } from '../core/hexIds.mjs'
|
|
8
8
|
import { pubKeyHash, verify } from '../crypto/crypto.mjs'
|
|
9
9
|
|
|
10
10
|
const BALLOT_DOMAIN = 'fount-owner-succession'
|
|
@@ -15,9 +15,9 @@ const BALLOT_DOMAIN = 'fount-owner-succession'
|
|
|
15
15
|
*/
|
|
16
16
|
export function ownerSuccessionBallotSignBytes(ballot) {
|
|
17
17
|
const body = {
|
|
18
|
-
proposedOwnerPubKeyHash:
|
|
19
|
-
groupId:
|
|
20
|
-
ballotId:
|
|
18
|
+
proposedOwnerPubKeyHash: normalizeHex64(ballot.proposedOwnerPubKeyHash),
|
|
19
|
+
groupId: ballot.groupId,
|
|
20
|
+
ballotId: ballot.ballotId,
|
|
21
21
|
}
|
|
22
22
|
return Buffer.from(`${BALLOT_DOMAIN}\0${canonicalStringify(body)}`, 'utf8')
|
|
23
23
|
}
|
|
@@ -42,8 +42,8 @@ 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 =
|
|
45
|
+
const pubKeyHex = normalizeHex64(entry.pubKeyHex)
|
|
46
|
+
const signatureHex = entry.signature
|
|
47
47
|
if (!isHex64(pubKeyHex) || !isSignatureHex128(signatureHex)) continue
|
|
48
48
|
|
|
49
49
|
const hash = pubKeyHash(Buffer.from(pubKeyHex, 'hex'))
|
package/index.mjs
CHANGED
|
@@ -18,11 +18,11 @@ import {
|
|
|
18
18
|
setSignalingRuntimeConfig,
|
|
19
19
|
} from './node/instance.mjs'
|
|
20
20
|
import { setConnectivityDebug } from './node/log.mjs'
|
|
21
|
+
import { loadReputation } from './node/reputation_store.mjs'
|
|
21
22
|
import {
|
|
22
23
|
attachReputationSyncWire,
|
|
23
24
|
getReputationExportAllowlist,
|
|
24
25
|
getReputationLocks,
|
|
25
|
-
getReputationTable,
|
|
26
26
|
getTrustSyncDonors,
|
|
27
27
|
lockReputationMax,
|
|
28
28
|
pullReputationFromNode,
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
ensureLinkToNode,
|
|
39
39
|
ensureOverlayRouter,
|
|
40
40
|
getLinkRegistry,
|
|
41
|
+
getPeerHealth,
|
|
42
|
+
listPeerHealth,
|
|
43
|
+
onPeerHealth,
|
|
41
44
|
reloadDiscoveryRelays,
|
|
42
45
|
sendToNodeLink,
|
|
43
46
|
} from './transport/link_registry.mjs'
|
|
@@ -66,16 +69,19 @@ export {
|
|
|
66
69
|
getLinkRegistry,
|
|
67
70
|
getNodeDir,
|
|
68
71
|
getNodeHash,
|
|
72
|
+
getPeerHealth,
|
|
69
73
|
getReputationExportAllowlist,
|
|
70
74
|
getReputationLocks,
|
|
71
|
-
getReputationTable,
|
|
72
75
|
getRoutingProfile,
|
|
73
76
|
getTrustSyncDonors,
|
|
74
77
|
getUserRoomSlot,
|
|
75
78
|
initNode,
|
|
76
79
|
isInfraRunning,
|
|
77
80
|
isNodeInitialized,
|
|
81
|
+
listPeerHealth,
|
|
82
|
+
loadReputation,
|
|
78
83
|
lockReputationMax,
|
|
84
|
+
onPeerHealth,
|
|
79
85
|
pullReputationFromNode,
|
|
80
86
|
registerDiscoveryProvider,
|
|
81
87
|
registerLinkProvider,
|
|
@@ -17,6 +17,6 @@ export function defaultNodeDir() {
|
|
|
17
17
|
* @returns {string} 解析后的绝对 node 目录
|
|
18
18
|
*/
|
|
19
19
|
export function resolveNodeDir(override) {
|
|
20
|
-
const
|
|
21
|
-
return
|
|
20
|
+
const value = override || ''
|
|
21
|
+
return value ? path.resolve(value) : defaultNodeDir()
|
|
22
22
|
}
|
package/infra/priority.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
-
import {
|
|
2
|
+
import { loadReputation } from '../node/reputation_store.mjs'
|
|
3
3
|
import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
|
|
4
4
|
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ export function applyPriorityToRegistry() {
|
|
|
39
39
|
}
|
|
40
40
|
registry.setPriorityWeightFunction(nodeHash => {
|
|
41
41
|
const score = pickNodeScoreFromReputation(
|
|
42
|
-
|
|
42
|
+
loadReputation(),
|
|
43
43
|
normalizeHex64(nodeHash) || nodeHash,
|
|
44
44
|
)
|
|
45
45
|
return Math.floor(score * PRIORITY_BOOST)
|
package/link/frame.mjs
CHANGED
|
@@ -25,11 +25,10 @@ function normalizeFrameIdBytes(frameId) {
|
|
|
25
25
|
throw new Error(`p2p: frameId must be ${FRAME_ID_BYTES} bytes`)
|
|
26
26
|
return frameId
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
if (text.length !== FRAME_ID_BYTES * 2)
|
|
28
|
+
if (frameId.length !== FRAME_ID_BYTES * 2)
|
|
30
29
|
throw new Error('p2p: frameId must be 32 hex characters')
|
|
31
30
|
try {
|
|
32
|
-
return hexToBytes(
|
|
31
|
+
return hexToBytes(frameId)
|
|
33
32
|
}
|
|
34
33
|
catch {
|
|
35
34
|
throw new Error('p2p: frameId must be 32 hex characters')
|
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, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
|
+
import { isHex64, isSignatureHex128, normalizeHex64 } 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'
|
|
@@ -117,9 +117,9 @@ export function parseHello(hello) {
|
|
|
117
117
|
export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
118
118
|
const parsedHello = parseHello(hello)
|
|
119
119
|
if (!parsedHello) return null
|
|
120
|
-
const signatureHex = String(auth?.sig ?? '')
|
|
120
|
+
const signatureHex = String(auth?.sig ?? '')
|
|
121
121
|
const binding = normalizeLinkBinding(remoteBinding)
|
|
122
|
-
if (
|
|
122
|
+
if (!isSignatureHex128(signatureHex) || !binding) return null
|
|
123
123
|
const normalizedNonce = normalizeHex64(expectedNonce)
|
|
124
124
|
if (!isHex64(normalizedNonce)) return null
|
|
125
125
|
const message = buildAuthMessage(normalizedNonce, binding, parsedHello.nodeHash)
|
|
@@ -141,7 +141,7 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
|
141
141
|
* @returns {Uint8Array} 待签名消息字节
|
|
142
142
|
*/
|
|
143
143
|
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null) {
|
|
144
|
-
const base = `fount-advert\0${
|
|
144
|
+
const base = `fount-advert\0${rendezvousKey}\0${ts}\0${normalizeHex64(nodeHash)}`
|
|
145
145
|
const port = normalizeTcpPort(tcpPort)
|
|
146
146
|
let message = port ? `${base}\0${port}` : base
|
|
147
147
|
const hosts = normalizeLanHosts(lanHosts)
|
|
@@ -194,8 +194,8 @@ export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now()
|
|
|
194
194
|
const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
|
|
195
195
|
if (!parsedHello) return null
|
|
196
196
|
const ts = Number(advert?.ts)
|
|
197
|
-
const sig = String(advert?.sig ?? '')
|
|
198
|
-
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs ||
|
|
197
|
+
const sig = String(advert?.sig ?? '')
|
|
198
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !isSignatureHex128(sig)) return null
|
|
199
199
|
const hasTcpPortField = !!advert?.tcpPort
|
|
200
200
|
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
201
201
|
if (hasTcpPortField && !tcpPort) return null
|
package/link/pipe.mjs
CHANGED
|
@@ -10,6 +10,11 @@ import { buildAuth, buildHello, parseHello, verifyAuth } from './handshake.mjs'
|
|
|
10
10
|
const encoder = new TextEncoder()
|
|
11
11
|
const decoder = new TextDecoder()
|
|
12
12
|
|
|
13
|
+
/** RTT 样本滑动窗口大小。 */
|
|
14
|
+
const RTT_WINDOW_SIZE = 10
|
|
15
|
+
/** 未收到 pong 的 ping 记录保留时长。 */
|
|
16
|
+
const PING_RECORD_TTL_MS = 30000
|
|
17
|
+
|
|
13
18
|
/**
|
|
14
19
|
* 把 createLinkPipe 句柄收成上层 LinkHandle(可附带测试/内部字段)。
|
|
15
20
|
* @param {ReturnType<typeof createLinkPipe>} pipe pipe 句柄
|
|
@@ -42,6 +47,11 @@ export function asLinkHandle(pipe, extras = {}) {
|
|
|
42
47
|
* @returns {ReturnType<typeof pipe.onDown>} 取消订阅
|
|
43
48
|
*/
|
|
44
49
|
onDown: (...callArguments) => pipe.onDown(...callArguments),
|
|
50
|
+
/**
|
|
51
|
+
* @param {...any} callArguments 透传 onRtt
|
|
52
|
+
* @returns {ReturnType<typeof pipe.onRtt>} 取消订阅
|
|
53
|
+
*/
|
|
54
|
+
onRtt: (...callArguments) => pipe.onRtt(...callArguments),
|
|
45
55
|
/**
|
|
46
56
|
* @param {...any} callArguments 透传 close
|
|
47
57
|
* @returns {ReturnType<typeof pipe.close>} 关闭完成
|
|
@@ -70,6 +80,7 @@ export function asLinkHandle(pipe, extras = {}) {
|
|
|
70
80
|
* @param {number} [options.heartbeatMs] 心跳间隔
|
|
71
81
|
* @param {number} [options.idleTimeoutMs] 空闲超时
|
|
72
82
|
* @param {number} [options.handshakeTimeoutMs] 握手超时
|
|
83
|
+
* @param {number} [options.rttWindowSize] RTT 样本滑动窗口大小
|
|
73
84
|
* @returns {object} link 句柄 + 入站 API
|
|
74
85
|
*/
|
|
75
86
|
export function createLinkPipe(options) {
|
|
@@ -96,6 +107,20 @@ export function createLinkPipe(options) {
|
|
|
96
107
|
let lastOutboundAt = 0
|
|
97
108
|
let sentFrames = 0
|
|
98
109
|
let recvFrames = 0
|
|
110
|
+
const rttWindowSize = Math.max(2, Math.floor(Number(options.rttWindowSize) || RTT_WINDOW_SIZE))
|
|
111
|
+
let pingSeq = 0
|
|
112
|
+
/** @type {Map<number, number>} seq -> 发送时间戳,未收到 pong 的 ping */
|
|
113
|
+
const outstandingPings = new Map()
|
|
114
|
+
let rttMs = null
|
|
115
|
+
let avgRttMs = null
|
|
116
|
+
let minRttMs = null
|
|
117
|
+
let maxRttMs = null
|
|
118
|
+
let pingCount = 0
|
|
119
|
+
let pongCount = 0
|
|
120
|
+
/** @type {number[]} */
|
|
121
|
+
const rttSamples = []
|
|
122
|
+
/** @type {Set<(rttMs: number) => void>} */
|
|
123
|
+
const rttListeners = new Set()
|
|
99
124
|
const envelopeListeners = new Set()
|
|
100
125
|
const downListeners = new Set()
|
|
101
126
|
const completedFrameIds = createLruMap(4096)
|
|
@@ -142,7 +167,14 @@ export function createLinkPipe(options) {
|
|
|
142
167
|
ready = true
|
|
143
168
|
clearTimeout(handshakeTimer)
|
|
144
169
|
heartbeatTimer = setInterval(() => {
|
|
145
|
-
|
|
170
|
+
pingCount++
|
|
171
|
+
const seq = pingSeq++
|
|
172
|
+
const ts = Date.now()
|
|
173
|
+
const cutoff = ts - PING_RECORD_TTL_MS
|
|
174
|
+
for (const [outSeq, outTs] of outstandingPings)
|
|
175
|
+
if (outTs < cutoff) outstandingPings.delete(outSeq)
|
|
176
|
+
outstandingPings.set(seq, ts)
|
|
177
|
+
void send({ scope: 'link', action: 'ping', payload: { ts, seq } }).catch(() => { })
|
|
146
178
|
}, heartbeatMs)
|
|
147
179
|
idleTimer = setInterval(() => {
|
|
148
180
|
if (Date.now() - lastInboundAt > idleTimeoutMs)
|
|
@@ -218,10 +250,29 @@ export function createLinkPipe(options) {
|
|
|
218
250
|
if (frameId) completedFrameIds.touch(frameId, true)
|
|
219
251
|
if (envelope?.scope === 'link') {
|
|
220
252
|
if (envelope.action === 'ping') {
|
|
221
|
-
|
|
253
|
+
const pingPayload = envelope.payload ?? {}
|
|
254
|
+
void send({ scope: 'link', action: 'pong', payload: { ts: pingPayload.ts, seq: pingPayload.seq } }).catch(() => { })
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (envelope.action === 'pong') {
|
|
258
|
+
const pongPayload = envelope.payload ?? {}
|
|
259
|
+
const sentTs = outstandingPings.get(pongPayload.seq)
|
|
260
|
+
if (Number.isFinite(pongPayload.ts) && sentTs !== undefined && sentTs === pongPayload.ts) {
|
|
261
|
+
outstandingPings.delete(pongPayload.seq)
|
|
262
|
+
pongCount++
|
|
263
|
+
const sample = Date.now() - sentTs
|
|
264
|
+
if (sample >= 0) {
|
|
265
|
+
rttMs = sample
|
|
266
|
+
rttSamples.push(sample)
|
|
267
|
+
if (rttSamples.length > rttWindowSize) rttSamples.shift()
|
|
268
|
+
avgRttMs = Math.round(rttSamples.reduce((total, value) => total + value, 0) / rttSamples.length)
|
|
269
|
+
minRttMs = rttSamples.reduce((minimum, sample) => Math.min(minimum, sample))
|
|
270
|
+
maxRttMs = rttSamples.reduce((minimum, sample) => Math.max(minimum, sample))
|
|
271
|
+
emitSafe(rttListeners, rttMs)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
222
274
|
return
|
|
223
275
|
}
|
|
224
|
-
if (envelope.action === 'pong') return
|
|
225
276
|
}
|
|
226
277
|
if (ready && remoteNodeHash)
|
|
227
278
|
emitSafe(envelopeListeners, envelope, remoteNodeHash)
|
|
@@ -299,8 +350,7 @@ export function createLinkPipe(options) {
|
|
|
299
350
|
}))
|
|
300
351
|
for (const frame of encodeFrames(frameId, bytes)) {
|
|
301
352
|
const sent = options.sendFrame(envelope.action, frame)
|
|
302
|
-
if (sent
|
|
303
|
-
await sent
|
|
353
|
+
if (sent?.then) await sent
|
|
304
354
|
sentFrames++
|
|
305
355
|
}
|
|
306
356
|
lastOutboundAt = Date.now()
|
|
@@ -351,6 +401,14 @@ export function createLinkPipe(options) {
|
|
|
351
401
|
downListeners.add(callback)
|
|
352
402
|
return () => downListeners.delete(callback)
|
|
353
403
|
},
|
|
404
|
+
/**
|
|
405
|
+
* @param {(rttMs: number) => void} callback RTT 样本回调
|
|
406
|
+
* @returns {() => void} 取消订阅
|
|
407
|
+
*/
|
|
408
|
+
onRtt(callback) {
|
|
409
|
+
rttListeners.add(callback)
|
|
410
|
+
return () => rttListeners.delete(callback)
|
|
411
|
+
},
|
|
354
412
|
close,
|
|
355
413
|
handleInbound,
|
|
356
414
|
startHandshake,
|
|
@@ -370,6 +428,12 @@ export function createLinkPipe(options) {
|
|
|
370
428
|
lastOutboundAt,
|
|
371
429
|
sentFrames,
|
|
372
430
|
recvFrames,
|
|
431
|
+
rttMs,
|
|
432
|
+
avgRttMs,
|
|
433
|
+
minRttMs,
|
|
434
|
+
maxRttMs,
|
|
435
|
+
pingCount,
|
|
436
|
+
pongCount,
|
|
373
437
|
closeReason,
|
|
374
438
|
...options.extraStats?.() ?? {},
|
|
375
439
|
}
|
|
@@ -15,14 +15,6 @@ export const BLE_DATA_SERVICE_UUID = 'f017f017f017f017f017f017f017f019'
|
|
|
15
15
|
export const BLE_DATA_CHAR_UUID = 'f017f017f017f017f017f017f017f01a'
|
|
16
16
|
const BT_DEVICE_NAME = 'fount-bt'
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
* 探测 BLE GATT 数据链路是否可用;失败则回落其它 link provider。
|
|
20
|
-
* @returns {Promise<boolean>} 可用为 true
|
|
21
|
-
*/
|
|
22
|
-
export async function canUseBleGattLink() {
|
|
23
|
-
return canUseBluetoothRuntime()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
18
|
/**
|
|
27
19
|
* 在 GATT write/notify 上建立 pipe。
|
|
28
20
|
* @param {object} options 配置
|
|
@@ -279,7 +271,7 @@ export function createBleGattLinkProvider() {
|
|
|
279
271
|
id: instanceId,
|
|
280
272
|
level: LINK_LEVEL_BLE_GATT,
|
|
281
273
|
caps: { needsOfferAnswer: false, needsDiscoverySignal: false, probe: 'native' },
|
|
282
|
-
isAvailable:
|
|
274
|
+
isAvailable: canUseBluetoothRuntime,
|
|
283
275
|
/**
|
|
284
276
|
* @param {{ nodeHash: string }} remote 远端
|
|
285
277
|
* @returns {boolean} 是否有 BT peer hint
|
package/link/providers/index.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const providers = new Map()
|
|
|
35
35
|
*/
|
|
36
36
|
export function registerLinkProvider(provider) {
|
|
37
37
|
if (!provider?.id) throw new Error('p2p: link provider requires id')
|
|
38
|
-
providers.set(
|
|
38
|
+
providers.set(provider.id, provider)
|
|
39
39
|
return () => unregisterLinkProvider(provider.id)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -45,7 +45,7 @@ export function registerLinkProvider(provider) {
|
|
|
45
45
|
* @returns {void}
|
|
46
46
|
*/
|
|
47
47
|
export function unregisterLinkProvider(id) {
|
|
48
|
-
providers.delete(
|
|
48
|
+
providers.delete(id)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
package/link/providers/nostr.mjs
CHANGED
|
@@ -48,9 +48,7 @@ async function publishLinkPacket(remoteNodeHash, packet) {
|
|
|
48
48
|
* @returns {import('./index.mjs').LinkProvider & { deliverPacket: (packet: object) => void }} provider
|
|
49
49
|
*/
|
|
50
50
|
export function createNostrLinkProvider(options = {}) {
|
|
51
|
-
const resolveRelayUrls =
|
|
52
|
-
? options.getRelayUrls
|
|
53
|
-
: resolveDefaultRelayUrls
|
|
51
|
+
const resolveRelayUrls = options.getRelayUrls || resolveDefaultRelayUrls
|
|
54
52
|
|
|
55
53
|
/** @type {((link: import('./index.mjs').LinkHandle) => void) | null} */
|
|
56
54
|
let onInbound = null
|
|
@@ -264,7 +264,7 @@ export async function createWebRtcLink(options) {
|
|
|
264
264
|
*/
|
|
265
265
|
channel.onmessage = event => {
|
|
266
266
|
try {
|
|
267
|
-
const data = event
|
|
267
|
+
const { data } = event
|
|
268
268
|
pipe.handleInbound(typeof data === 'string' ? data : toBytes(data, { allowString: true }))
|
|
269
269
|
}
|
|
270
270
|
catch { /* drop */ }
|
|
@@ -304,7 +304,7 @@ export async function createWebRtcLink(options) {
|
|
|
304
304
|
if (trickleIceOff || !event.candidate) return
|
|
305
305
|
void sendSignal({
|
|
306
306
|
type: 'ice',
|
|
307
|
-
candidate:
|
|
307
|
+
candidate: event.candidate.toJSON?.() || event.candidate,
|
|
308
308
|
}).catch(error => pipe.close(`signal-send-failed:${formatErrorReason(error)}`))
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @returns {string | null} 处理后的 SDP;drop 策略下不可用时返回 null
|
|
12
12
|
*/
|
|
13
13
|
export function applyIceLocalHostnamePolicy(candidateSdp, policy) {
|
|
14
|
-
const sdp =
|
|
14
|
+
const sdp = candidateSdp || ''
|
|
15
15
|
if (!sdp || !/\.local/i.test(sdp)) return sdp || null
|
|
16
16
|
if (!/\btyp host\b/i.test(sdp)) return sdp
|
|
17
17
|
if (policy === 'drop') return null
|
|
@@ -34,9 +34,10 @@ export function filterIceLocalHostnameCandidate(candidate, RTCIceCandidateCtor,
|
|
|
34
34
|
const rewritten = applyIceLocalHostnamePolicy(raw, policy)
|
|
35
35
|
if (!rewritten) return null
|
|
36
36
|
if (rewritten === raw) return candidate
|
|
37
|
-
const init =
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const init = {
|
|
38
|
+
...candidate.toJSON?.() || { sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex },
|
|
39
|
+
candidate: rewritten,
|
|
40
|
+
}
|
|
40
41
|
return new RTCIceCandidateCtor(init)
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -49,7 +50,7 @@ export function filterIceLocalHostnameCandidate(candidate, RTCIceCandidateCtor,
|
|
|
49
50
|
export function wrapRtcPeerConnectionForIceLocalHostname(BaseRTC, RTCIceCandidate, policy = 'drop') {
|
|
50
51
|
if (policy === 'none') return BaseRTC
|
|
51
52
|
|
|
52
|
-
const baseRoutesIce =
|
|
53
|
+
const baseRoutesIce = !!BaseRTC.prototype.prepareIceCandidateEvent
|
|
53
54
|
|
|
54
55
|
return class IceLocalHostnameFilteredRTCPeerConnection extends BaseRTC {
|
|
55
56
|
/** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
|
package/link/rtc/w3c_bridge.mjs
CHANGED
|
@@ -36,7 +36,7 @@ function defineEmitterHandler(target, property, eventName, adapt = payload => pa
|
|
|
36
36
|
* @returns {RTCDataChannel} 已挂 W3C handler 的通道
|
|
37
37
|
*/
|
|
38
38
|
export function bridgeDataChannel(channel) {
|
|
39
|
-
if (bridgedChannels.has(channel) ||
|
|
39
|
+
if (bridgedChannels.has(channel) || !channel.on) return channel
|
|
40
40
|
bridgedChannels.add(channel)
|
|
41
41
|
defineEmitterHandler(channel, 'onmessage', 'message')
|
|
42
42
|
defineEmitterHandler(channel, 'onopen', 'open')
|
package/link/sdp_fingerprint.mjs
CHANGED
|
@@ -20,6 +20,6 @@ export function normalizeDtlsFingerprint(value) {
|
|
|
20
20
|
* @returns {string | null} 规范化 fingerprint,未找到时返回 null
|
|
21
21
|
*/
|
|
22
22
|
export function extractDtlsFingerprint(sdp) {
|
|
23
|
-
const line =
|
|
23
|
+
const line = sdp.match(/^a=fingerprint:sha-256\s+([\d:A-Fa-f]+)$/mu)?.[1]
|
|
24
24
|
return normalizeDtlsFingerprint(line)
|
|
25
25
|
}
|
|
@@ -19,7 +19,7 @@ const consumers = new Map()
|
|
|
19
19
|
* @returns {void}
|
|
20
20
|
*/
|
|
21
21
|
export function registerMailboxConsumer(app, handler) {
|
|
22
|
-
consumers.set(
|
|
22
|
+
consumers.set(app, { handler })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -27,7 +27,7 @@ export function registerMailboxConsumer(app, handler) {
|
|
|
27
27
|
* @returns {void}
|
|
28
28
|
*/
|
|
29
29
|
export function unregisterMailboxConsumer(app) {
|
|
30
|
-
consumers.delete(
|
|
30
|
+
consumers.delete(app)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -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 = entry?.remoteNodeHash
|
|
28
|
+
const remote = normalizeHex64(entry?.remoteNodeHash)
|
|
29
29
|
return remote || null
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -76,7 +76,7 @@ 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 = options.toNodeHash
|
|
79
|
+
const toNodeHash = normalizeHex64(options.toNodeHash)
|
|
80
80
|
const delivered = toNodeHash && isMailboxRecordWithinSizeLimit(record)
|
|
81
81
|
? await requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, toNodeHash, 'mailbox_put', { nodeHash, record })
|
|
82
82
|
: false
|
|
@@ -116,7 +116,7 @@ export async function ingestMailboxPut(wireContext, put, peerId = '') {
|
|
|
116
116
|
if (!record?.envelope || !record?.toPubKeyHash) return
|
|
117
117
|
const fromNode = normalizeHex64(put.nodeHash)
|
|
118
118
|
if (!fromNode || !takeIncomingMailboxPutSlot(fromNode)) return
|
|
119
|
-
const username =
|
|
119
|
+
const username = wireContext.replicaUsername || ''
|
|
120
120
|
if (!username) return
|
|
121
121
|
if (peerId) {
|
|
122
122
|
const remote = await resolveRemoteNodeHashForPeer(username, peerId)
|
|
@@ -162,7 +162,7 @@ export async function respondMailboxWant(want, sendGive, peerId) {
|
|
|
162
162
|
export async function ingestMailboxGive(wireContext, give) {
|
|
163
163
|
const records = (give.records || []).filter(isDeliverableMailboxRecord)
|
|
164
164
|
if (!records.length) return 0
|
|
165
|
-
const username =
|
|
165
|
+
const username = wireContext.replicaUsername || ''
|
|
166
166
|
if (!username) return 0
|
|
167
167
|
const { dispatchMailboxRecordsToConsumers } = await import('./consumer_registry.mjs')
|
|
168
168
|
const { deleteMailboxRecords } = await import('./store.mjs')
|
package/mailbox/parse.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export function parseMailboxGive(payload) {
|
|
|
64
64
|
return { ok: false, code: shape.code, field: `records[${i}].${shape.field}` }
|
|
65
65
|
if (!record.envelope || typeof record.envelope !== 'object')
|
|
66
66
|
return { ok: false, code: 'required', field: `records[${i}].envelope` }
|
|
67
|
-
const app = String(record.app || '')
|
|
67
|
+
const app = String(record.app || '')
|
|
68
68
|
if (!app)
|
|
69
69
|
return { ok: false, code: 'required', field: `records[${i}].app` }
|
|
70
70
|
records.push(record)
|
package/mailbox/prune.mjs
CHANGED
|
@@ -32,8 +32,8 @@ export function mailboxRecordBytes(record) {
|
|
|
32
32
|
*/
|
|
33
33
|
export function mailboxBucketKey(record) {
|
|
34
34
|
const to = normalizeHex64(record.toPubKeyHash) || ''
|
|
35
|
-
const from = record.fromNodeHash
|
|
36
|
-
|| record.envelope?.sender
|
|
35
|
+
const from = normalizeHex64(record.fromNodeHash)
|
|
36
|
+
|| normalizeHex64(record.envelope?.sender)
|
|
37
37
|
|| 'unknown'
|
|
38
38
|
return `${to}\0${from}`
|
|
39
39
|
}
|
package/mailbox/rate.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mailbox 入站 put 限速(按来源节点,节点级单例)。
|
|
3
3
|
*/
|
|
4
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
5
|
|
|
5
6
|
const DEFAULT_WINDOW_MS = 60_000
|
|
6
7
|
const DEFAULT_MAX_PUTS = 20
|
|
@@ -14,21 +15,13 @@ const inboundByKey = new Map()
|
|
|
14
15
|
* @param {object} [limits] 可选限额
|
|
15
16
|
* @returns {{ windowMs: number, maxPuts: number }} 生效限额
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
+
function resolveMailboxRateLimits(limits = {}) {
|
|
18
19
|
return {
|
|
19
20
|
windowMs: Math.max(1000, Number(limits.windowMs) || DEFAULT_WINDOW_MS),
|
|
20
21
|
maxPuts: Math.max(1, Math.min(256, Number(limits.maxPuts) || DEFAULT_MAX_PUTS)),
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} fromNodeHash 来源节点
|
|
26
|
-
* @returns {string} 限速键
|
|
27
|
-
*/
|
|
28
|
-
export function mailboxRateKey(fromNodeHash) {
|
|
29
|
-
return String(fromNodeHash || '').trim()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
/**
|
|
33
26
|
* @param {number} now 当前时间戳
|
|
34
27
|
* @returns {void}
|
|
@@ -69,7 +62,7 @@ function evictLruIfNeeded(key) {
|
|
|
69
62
|
*/
|
|
70
63
|
export function takeIncomingMailboxPutSlot(fromNodeHash, limits) {
|
|
71
64
|
const { windowMs, maxPuts } = resolveMailboxRateLimits(limits)
|
|
72
|
-
const key =
|
|
65
|
+
const key = normalizeHex64(fromNodeHash)
|
|
73
66
|
const now = Date.now()
|
|
74
67
|
if (inboundByKey.size >= MAX_KEYS) sweepExpiredEntries(now)
|
|
75
68
|
evictLruIfNeeded(key)
|