@steve02081504/fount-p2p 0.0.5 → 0.0.7
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/core/tcp_port.mjs +11 -0
- package/dag/canonicalize_row.mjs +5 -5
- package/discovery/advert_peer_hints.mjs +21 -0
- package/discovery/{bt.mjs → bt/index.mjs} +151 -55
- package/discovery/bt/peer_hints.mjs +41 -0
- package/discovery/bt/runtime.mjs +46 -0
- package/discovery/lan_peer_hints.mjs +43 -0
- package/discovery/mdns.mjs +10 -6
- package/discovery/nostr.mjs +3 -3
- package/federation/chunk_fetch_pending.mjs +3 -3
- package/federation/chunk_fetch_scheduler.mjs +3 -3
- package/federation/dag_order_cache.mjs +3 -3
- package/federation/topo_order_memo.mjs +3 -3
- package/files/chunk_responder.mjs +1 -1
- package/files/evfs.mjs +26 -26
- package/files/transfer_key.mjs +9 -9
- package/files/transfer_key_registry.mjs +9 -9
- package/governance/join_pow.mjs +17 -17
- package/index.mjs +3 -2
- package/link/channel_mux.mjs +7 -7
- package/link/frame.mjs +5 -5
- package/link/handshake.mjs +63 -36
- package/link/pipe.mjs +404 -0
- package/link/providers/ble_gatt.mjs +339 -0
- package/link/providers/index.mjs +90 -0
- package/link/providers/lan_tcp.mjs +342 -0
- package/link/providers/levels.mjs +9 -0
- package/link/providers/webrtc.mjs +385 -0
- package/mailbox/deliver_or_store.mjs +13 -13
- package/mailbox/wire.mjs +4 -4
- package/node/reputation_store.mjs +5 -5
- package/node/retention_policy.mjs +8 -8
- package/overlay/index.mjs +6 -6
- package/package.json +3 -2
- package/registries/inbound.mjs +8 -8
- package/rooms/scoped_link.mjs +23 -21
- package/timeline/append_core.mjs +3 -3
- package/transport/group_link_set.mjs +35 -33
- package/transport/link_registry.mjs +250 -63
- package/transport/peer_pool.mjs +4 -4
- package/transport/user_room.mjs +12 -14
- package/utils/ttl_map.mjs +41 -0
- package/wire/group_part.mjs +3 -3
- package/wire/part_ingress.mjs +14 -14
- package/wire/part_query.mjs +81 -82
- package/wire/part_query_cache.mjs +7 -0
- package/link/link.mjs +0 -617
package/files/evfs.mjs
CHANGED
|
@@ -10,21 +10,21 @@ import { fetchChunk } from './chunk_fetch.mjs'
|
|
|
10
10
|
import { createChunkReadStream, getChunk, hasChunk, putChunk } from './chunk_store.mjs'
|
|
11
11
|
import { normalizeFileManifest, publicTransferKeyDescriptor } from './manifest.mjs'
|
|
12
12
|
import { assembleManifestPlaintext, resolveContentKey } from './transfer_key.mjs'
|
|
13
|
-
import { readDagManifestPlaintext,
|
|
13
|
+
import { readDagManifestPlaintext, resolveTransferKeyDependencies } from './transfer_key_registry.mjs'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {string} replicaUsername 副本用户名
|
|
17
17
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
18
18
|
* @returns {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} 密钥依赖
|
|
19
19
|
*/
|
|
20
|
-
function
|
|
21
|
-
const
|
|
20
|
+
function transferKeyDependenciesForReplica(replicaUsername, manifest) {
|
|
21
|
+
const rawDependencies = resolveTransferKeyDependencies(undefined, manifest)
|
|
22
22
|
return {
|
|
23
|
-
getGroupFileMasterKey:
|
|
24
|
-
? (groupId, keyGeneration) =>
|
|
23
|
+
getGroupFileMasterKey: rawDependencies.getGroupFileMasterKey
|
|
24
|
+
? (groupId, keyGeneration) => rawDependencies.getGroupFileMasterKey(replicaUsername, groupId, keyGeneration)
|
|
25
25
|
: undefined,
|
|
26
|
-
getVaultMasterKey:
|
|
27
|
-
? entityHash =>
|
|
26
|
+
getVaultMasterKey: rawDependencies.getVaultMasterKey
|
|
27
|
+
? entityHash => rawDependencies.getVaultMasterKey(replicaUsername, entityHash)
|
|
28
28
|
: undefined,
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -32,13 +32,13 @@ function transferKeyDepsForReplica(replicaUsername, manifest) {
|
|
|
32
32
|
/**
|
|
33
33
|
* @param {string} username 拉取身份
|
|
34
34
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
35
|
-
* @param {{ fetchChunk?: Function }} [
|
|
35
|
+
* @param {{ fetchChunk?: Function }} [options] miss 拉取
|
|
36
36
|
* @returns {Promise<boolean>} 全部 part 是否已就位
|
|
37
37
|
*/
|
|
38
|
-
async function ensureManifestPartsLocal(username, manifest,
|
|
38
|
+
async function ensureManifestPartsLocal(username, manifest, options = {}) {
|
|
39
39
|
for (const part of manifest.parts) {
|
|
40
40
|
if (await hasChunk(part.hash)) continue
|
|
41
|
-
const fetchedChunk = await (
|
|
41
|
+
const fetchedChunk = await (options.fetchChunk || fetchChunk)({
|
|
42
42
|
username,
|
|
43
43
|
ciphertextHash: part.hash,
|
|
44
44
|
ownerEntityHash: manifest.ownerEntityHash,
|
|
@@ -81,46 +81,46 @@ export async function storeManifestParts(manifest, partBytes) {
|
|
|
81
81
|
/**
|
|
82
82
|
* @param {string} replicaUsername 副本用户名
|
|
83
83
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
84
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
84
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
85
85
|
* @returns {Promise<Buffer | null>} 明文内容
|
|
86
86
|
*/
|
|
87
|
-
export async function readManifestPlaintext(replicaUsername, manifest,
|
|
87
|
+
export async function readManifestPlaintext(replicaUsername, manifest, options = {}) {
|
|
88
88
|
const dagGroupId = manifest.meta?.groupId
|
|
89
89
|
if (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
|
|
90
90
|
const dagPlain = await readDagManifestPlaintext(replicaUsername, manifest)
|
|
91
91
|
if (dagPlain) return dagPlain
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const username =
|
|
95
|
-
if (!await ensureManifestPartsLocal(username, manifest,
|
|
94
|
+
const username = options.username || replicaUsername
|
|
95
|
+
if (!await ensureManifestPartsLocal(username, manifest, options)) return null
|
|
96
96
|
|
|
97
97
|
/** @type {Buffer[]} */
|
|
98
98
|
const partBytes = []
|
|
99
99
|
for (const part of manifest.parts)
|
|
100
100
|
partBytes.push(await getChunk(part.hash))
|
|
101
101
|
|
|
102
|
-
return assembleManifestPlaintext(manifest, partBytes,
|
|
102
|
+
return assembleManifestPlaintext(manifest, partBytes, transferKeyDependenciesForReplica(replicaUsername, manifest))
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* @param {string} replicaUsername 副本用户名
|
|
107
107
|
* @param {import('./manifest.mjs').FileManifest} manifest 清单
|
|
108
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
108
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
109
109
|
* @returns {Promise<import('node:stream').Readable | null>} 明文流
|
|
110
110
|
*/
|
|
111
|
-
export async function readManifestPlaintextStream(replicaUsername, manifest,
|
|
111
|
+
export async function readManifestPlaintextStream(replicaUsername, manifest, options = {}) {
|
|
112
112
|
const dagGroupId = manifest.meta?.groupId
|
|
113
113
|
if (Array.isArray(manifest.meta?.dagParts) && dagGroupId) {
|
|
114
|
-
const plain = await readManifestPlaintext(replicaUsername, manifest,
|
|
114
|
+
const plain = await readManifestPlaintext(replicaUsername, manifest, options)
|
|
115
115
|
if (!plain) return null
|
|
116
116
|
return Readable.from([plain])
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const username =
|
|
120
|
-
if (!await ensureManifestPartsLocal(username, manifest,
|
|
119
|
+
const username = options.username || replicaUsername
|
|
120
|
+
if (!await ensureManifestPartsLocal(username, manifest, options)) return null
|
|
121
121
|
|
|
122
|
-
const
|
|
123
|
-
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest,
|
|
122
|
+
const dependencies = transferKeyDependenciesForReplica(replicaUsername, manifest)
|
|
123
|
+
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, dependencies)
|
|
124
124
|
if (manifest.ceMode === 'random' && !contentKey) return null
|
|
125
125
|
|
|
126
126
|
const partStreams = manifest.parts.map(part => createChunkReadStream(part.hash))
|
|
@@ -219,16 +219,16 @@ export async function putFileManifestFromStream(params) {
|
|
|
219
219
|
* @param {string} replicaUsername 副本用户名
|
|
220
220
|
* @param {string} entityHash owner entityHash
|
|
221
221
|
* @param {string} logicalPath EVFS 逻辑路径
|
|
222
|
-
* @param {{ username?: string, fetchChunk?: Function }} [
|
|
222
|
+
* @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
|
|
223
223
|
* @returns {Promise<Buffer | null>} 明文或 null
|
|
224
224
|
*/
|
|
225
|
-
export async function readPublicFile(replicaUsername, entityHash, logicalPath,
|
|
225
|
+
export async function readPublicFile(replicaUsername, entityHash, logicalPath, options = {}) {
|
|
226
226
|
const { fetchPublicManifest } = await import('./manifest_fetch.mjs')
|
|
227
227
|
const manifest = await fetchPublicManifest({
|
|
228
|
-
username:
|
|
228
|
+
username: options.username || replicaUsername,
|
|
229
229
|
ownerEntityHash: entityHash,
|
|
230
230
|
logicalPath,
|
|
231
231
|
})
|
|
232
232
|
if (!manifest) return null
|
|
233
|
-
return readManifestPlaintext(replicaUsername, manifest,
|
|
233
|
+
return readManifestPlaintext(replicaUsername, manifest, options)
|
|
234
234
|
}
|
package/files/transfer_key.mjs
CHANGED
|
@@ -15,26 +15,26 @@ import {
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {TransferKeyDescriptor} descriptor 传递密钥描述符
|
|
17
17
|
* @param {FileManifest} manifest manifest
|
|
18
|
-
* @param {{ getGroupFileMasterKey?: (groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (entityHash: string) => Promise<Buffer | string | null> }}
|
|
18
|
+
* @param {{ getGroupFileMasterKey?: (groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (entityHash: string) => Promise<Buffer | string | null> }} dependencies 密钥源
|
|
19
19
|
* @returns {Promise<Buffer | null>} contentKey;plain/convergent 返回 null(按 contentHash 派生)
|
|
20
20
|
*/
|
|
21
|
-
export async function resolveContentKey(descriptor, manifest,
|
|
21
|
+
export async function resolveContentKey(descriptor, manifest, dependencies = {}) {
|
|
22
22
|
const type = descriptor?.type || 'public'
|
|
23
23
|
if (type === 'public' || manifest.ceMode === 'plain' || manifest.ceMode === 'convergent')
|
|
24
24
|
return null
|
|
25
25
|
|
|
26
26
|
if (type === 'file-master-key-wrap') {
|
|
27
27
|
const { groupId, fileId } = descriptor
|
|
28
|
-
if (!groupId || !fileId || !descriptor.wrappedKey || !
|
|
29
|
-
const groupKey = await
|
|
28
|
+
if (!groupId || !fileId || !descriptor.wrappedKey || !dependencies.getGroupFileMasterKey) return null
|
|
29
|
+
const groupKey = await dependencies.getGroupFileMasterKey(String(groupId), descriptor.keyGeneration)
|
|
30
30
|
if (!groupKey) return null
|
|
31
31
|
return unwrapContentKey(descriptor.wrappedKey, groupKey, fileId)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (type === 'vault-wrap') {
|
|
35
35
|
const { entityHash, fileId } = descriptor
|
|
36
|
-
if (!entityHash || !fileId || !descriptor.wrappedKey || !
|
|
37
|
-
const vaultKey = await
|
|
36
|
+
if (!entityHash || !fileId || !descriptor.wrappedKey || !dependencies.getVaultMasterKey) return null
|
|
37
|
+
const vaultKey = await dependencies.getVaultMasterKey(String(entityHash))
|
|
38
38
|
if (!vaultKey) return null
|
|
39
39
|
return unwrapContentKey(descriptor.wrappedKey, vaultKey, fileId)
|
|
40
40
|
}
|
|
@@ -70,12 +70,12 @@ export function decryptPart(encryptedPartBytes, manifest, contentKey, partIndex
|
|
|
70
70
|
/**
|
|
71
71
|
* @param {FileManifest} manifest manifest
|
|
72
72
|
* @param {Array<Buffer | Uint8Array>} partBytes 按序密文块
|
|
73
|
-
* @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }}
|
|
73
|
+
* @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} dependencies 密钥源
|
|
74
74
|
* @returns {Promise<Buffer | null>} 完整明文
|
|
75
75
|
*/
|
|
76
|
-
export async function assembleManifestPlaintext(manifest, partBytes,
|
|
76
|
+
export async function assembleManifestPlaintext(manifest, partBytes, dependencies = {}) {
|
|
77
77
|
if (partBytes.length !== manifest.parts.length) return null
|
|
78
|
-
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest,
|
|
78
|
+
const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, dependencies)
|
|
79
79
|
/** @type {Buffer[]} */
|
|
80
80
|
const plains = []
|
|
81
81
|
for (let index = 0; index < manifest.parts.length; index++) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/** @type {Map<string, { getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }>} */
|
|
2
|
-
const
|
|
2
|
+
const transferDependenciesByOwner = new Map()
|
|
3
3
|
|
|
4
4
|
/** @type {Map<string, (replicaUsername: string, manifest: import('./manifest.mjs').FileManifest) => Promise<Buffer | null>>} */
|
|
5
5
|
const dagPlaintextReadersByOwner = new Map()
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {string} ownerId 注册方
|
|
9
|
-
* @param {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }}
|
|
9
|
+
* @param {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} dependencies 密钥源
|
|
10
10
|
* @returns {void}
|
|
11
11
|
*/
|
|
12
|
-
export function
|
|
12
|
+
export function registerTransferKeyDependencies(ownerId, dependencies) {
|
|
13
13
|
const key = String(ownerId)
|
|
14
|
-
const prev =
|
|
15
|
-
|
|
14
|
+
const prev = transferDependenciesByOwner.get(key) || {}
|
|
15
|
+
transferDependenciesByOwner.set(key, { ...prev, ...dependencies })
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -50,9 +50,9 @@ export function unregisterManifestOwnerMatchers(ownerId) {
|
|
|
50
50
|
* @param {string} ownerId 注册方
|
|
51
51
|
* @returns {void}
|
|
52
52
|
*/
|
|
53
|
-
export function
|
|
53
|
+
export function unregisterTransferKeyDependencies(ownerId) {
|
|
54
54
|
const key = String(ownerId)
|
|
55
|
-
|
|
55
|
+
transferDependenciesByOwner.delete(key)
|
|
56
56
|
dagPlaintextReadersByOwner.delete(key)
|
|
57
57
|
unregisterManifestOwnerMatchers(key)
|
|
58
58
|
}
|
|
@@ -73,9 +73,9 @@ export function resolveManifestTransferOwnerId(manifest) {
|
|
|
73
73
|
* @param {import('./manifest.mjs').FileManifest} [manifest] 用于推断 ownerId
|
|
74
74
|
* @returns {{ getGroupFileMasterKey?: (replicaUsername: string, groupId: string, keyGeneration?: number) => Promise<Buffer | string | null>, getVaultMasterKey?: (replicaUsername: string, entityHash: string) => Promise<Buffer | string | null> }} 依赖
|
|
75
75
|
*/
|
|
76
|
-
export function
|
|
76
|
+
export function resolveTransferKeyDependencies(ownerId, manifest) {
|
|
77
77
|
const resolved = ownerId || (manifest ? resolveManifestTransferOwnerId(manifest) : null)
|
|
78
|
-
if (resolved) return
|
|
78
|
+
if (resolved) return transferDependenciesByOwner.get(String(resolved)) || {}
|
|
79
79
|
return {}
|
|
80
80
|
}
|
|
81
81
|
|
package/governance/join_pow.mjs
CHANGED
|
@@ -85,40 +85,40 @@ export function powVoluntaryBonus(achievedBits, floorBits, tunables = admissionT
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* @param {object} powSolution 客户端 solution
|
|
88
|
-
* @param {object}
|
|
89
|
-
* @param {string}
|
|
90
|
-
* @param {string}
|
|
91
|
-
* @param {string[]}
|
|
92
|
-
* @param {number} [
|
|
93
|
-
* @param {number} [
|
|
94
|
-
* @param {number} [
|
|
95
|
-
* @param {number} [
|
|
88
|
+
* @param {object} options 校验上下文
|
|
89
|
+
* @param {string} options.groupId 群 ID
|
|
90
|
+
* @param {string} options.senderNodeHash 签名者 nodeHash(须与 joiner 绑定)
|
|
91
|
+
* @param {string[]} options.knownAnchors 近期 tip / checkpoint root 列表
|
|
92
|
+
* @param {number} [options.now=Date.now()] 当前时间
|
|
93
|
+
* @param {number} [options.difficultyBits=0] 准入 floor(前导零 bit)
|
|
94
|
+
* @param {number} [options.epochMs=JOIN_POW_DEFAULT_EPOCH_MS] epoch 长度
|
|
95
|
+
* @param {number} [options.epochSkew=JOIN_POW_DEFAULT_EPOCH_SKEW] 允许 epoch 偏移
|
|
96
96
|
* @returns {{ ok: boolean, achievedBits: number }} 校验结果与实际达成 bit
|
|
97
97
|
*/
|
|
98
|
-
export function verifyJoinPow(powSolution,
|
|
99
|
-
const floorBits = Math.max(0, Math.floor(Number(
|
|
98
|
+
export function verifyJoinPow(powSolution, options) {
|
|
99
|
+
const floorBits = Math.max(0, Math.floor(Number(options.difficultyBits) || 0))
|
|
100
100
|
if (floorBits <= 0) return { ok: true, achievedBits: 0 }
|
|
101
101
|
if (!powSolution || typeof powSolution !== 'object') return { ok: false, achievedBits: 0 }
|
|
102
102
|
|
|
103
103
|
const anchorRef = String(powSolution.anchorRef ?? '').trim()
|
|
104
104
|
const { nonce } = powSolution
|
|
105
105
|
const epoch = Number(powSolution.epoch)
|
|
106
|
-
const joinerNodeHash = String(powSolution.joinerNodeHash ??
|
|
107
|
-
const senderNodeHash = String(
|
|
106
|
+
const joinerNodeHash = String(powSolution.joinerNodeHash ?? options.senderNodeHash ?? '').trim().toLowerCase()
|
|
107
|
+
const senderNodeHash = String(options.senderNodeHash ?? '').trim().toLowerCase()
|
|
108
108
|
|
|
109
109
|
if (!anchorRef || nonce == null || !Number.isFinite(epoch)) return { ok: false, achievedBits: 0 }
|
|
110
110
|
if (!joinerNodeHash || joinerNodeHash !== senderNodeHash) return { ok: false, achievedBits: 0 }
|
|
111
111
|
|
|
112
|
-
const anchors = (
|
|
112
|
+
const anchors = (options.knownAnchors ?? []).map(a => String(a).trim()).filter(Boolean)
|
|
113
113
|
if (!anchors.length || !anchors.includes(anchorRef)) return { ok: false, achievedBits: 0 }
|
|
114
114
|
|
|
115
|
-
const epochMs = Math.max(60_000, Number(
|
|
116
|
-
const skew = Math.max(0, Math.floor(Number(
|
|
117
|
-
const nowEpoch = Math.floor((
|
|
115
|
+
const epochMs = Math.max(60_000, Number(options.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
|
|
116
|
+
const skew = Math.max(0, Math.floor(Number(options.epochSkew) ?? JOIN_POW_DEFAULT_EPOCH_SKEW))
|
|
117
|
+
const nowEpoch = Math.floor((options.now ?? Date.now()) / epochMs)
|
|
118
118
|
if (Math.abs(epoch - nowEpoch) > skew) return { ok: false, achievedBits: 0 }
|
|
119
119
|
|
|
120
120
|
const hash = computeJoinPowHash({
|
|
121
|
-
groupId:
|
|
121
|
+
groupId: options.groupId,
|
|
122
122
|
anchorRef,
|
|
123
123
|
joinerNodeHash,
|
|
124
124
|
epoch,
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Federation P2P
|
|
3
|
-
*
|
|
2
|
+
* Federation P2P 门面:fount 网络引导与房间/发现入口。
|
|
3
|
+
* 上层只面对 nodeHash + envelope,不选择 WebRTC/BLE 等传输。
|
|
4
|
+
* 重型子系统请从子路径导入(如 `./dag`);勿导入未导出的 `link/`。
|
|
4
5
|
*/
|
|
5
6
|
import { registerDiscoveryProvider } from './discovery/index.mjs'
|
|
6
7
|
import { ensureNodeDefaults, getNodeHash } from './node/identity.mjs'
|
package/link/channel_mux.mjs
CHANGED
|
@@ -110,15 +110,15 @@ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CH
|
|
|
110
110
|
/**
|
|
111
111
|
* 订阅 bufferedamountlow 事件,返回取消订阅函数。
|
|
112
112
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
113
|
-
* @param {() => void}
|
|
113
|
+
* @param {() => void} callback 低水位回调
|
|
114
114
|
* @returns {() => void} 取消订阅函数
|
|
115
115
|
*/
|
|
116
|
-
export function onBufferedAmountLow(channel,
|
|
116
|
+
export function onBufferedAmountLow(channel, callback) {
|
|
117
117
|
/**
|
|
118
118
|
* bufferedamountlow 事件处理函数。
|
|
119
119
|
* @returns {void}
|
|
120
120
|
*/
|
|
121
|
-
const handler = () =>
|
|
121
|
+
const handler = () => callback()
|
|
122
122
|
channel.addEventListener?.('bufferedamountlow', handler)
|
|
123
123
|
channel.onbufferedamountlow = handler
|
|
124
124
|
if (channel.bufferedAmountLow?.subscribe)
|
|
@@ -131,12 +131,12 @@ export function onBufferedAmountLow(channel, cb) {
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* 创建带优先级队列的双通道发送器(control/bulk)。
|
|
134
|
-
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }}
|
|
134
|
+
* @param {{ getChannel: (name: 'control' | 'bulk') => RTCDataChannel | null | undefined, highWatermarkBytes?: number }} options 通道访问与高水位配置
|
|
135
135
|
* @returns {{ enqueue: (action: string, bytes: Uint8Array, preferredChannel?: 'control' | 'bulk') => void, flush: (channelName?: 'control' | 'bulk') => void, pending: () => { control: number, bulk: number }, clear: () => void }} 发送队列 API
|
|
136
136
|
*/
|
|
137
|
-
export function createChannelSendQueues(
|
|
138
|
-
const { getChannel } =
|
|
139
|
-
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(
|
|
137
|
+
export function createChannelSendQueues(options) {
|
|
138
|
+
const { getChannel } = options
|
|
139
|
+
const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(options.highWatermarkBytes) || CHANNEL_HIGH_WATERMARK_BYTES)
|
|
140
140
|
/** @type {{ control: Array<{ priority: number, seq: number, bytes: Uint8Array }>, bulk: Array<{ priority: number, seq: number, bytes: Uint8Array }> }} */
|
|
141
141
|
const queues = { control: [], bulk: [] }
|
|
142
142
|
/** @type {{ control: boolean, bulk: boolean }} */
|
package/link/frame.mjs
CHANGED
|
@@ -150,13 +150,13 @@ function concatChunks(chunks) {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* 创建分片消息重组器。
|
|
153
|
-
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [
|
|
153
|
+
* @param {{ maxMessageBytes?: number, maxPartials?: number, partialTimeoutMs?: number }} [options] 大小、并发分片数与超时配置
|
|
154
154
|
* @returns {{ push: (frame: Uint8Array | ArrayBuffer | ArrayBufferView, now?: number) => Uint8Array | null, prune: (now?: number) => string[], clear: () => void, size: () => number }} 重组器 API
|
|
155
155
|
*/
|
|
156
|
-
export function createReassembler(
|
|
157
|
-
const maxMessageBytes = Math.max(1024, Number(
|
|
158
|
-
const maxPartials = Math.max(1, Number(
|
|
159
|
-
const partialTimeoutMs = Math.max(1000, Number(
|
|
156
|
+
export function createReassembler(options = {}) {
|
|
157
|
+
const maxMessageBytes = Math.max(1024, Number(options.maxMessageBytes) || DEFAULT_MAX_MESSAGE_BYTES)
|
|
158
|
+
const maxPartials = Math.max(1, Number(options.maxPartials) || DEFAULT_MAX_PARTIAL_MESSAGES)
|
|
159
|
+
const partialTimeoutMs = Math.max(1000, Number(options.partialTimeoutMs) || DEFAULT_PARTIAL_TIMEOUT_MS)
|
|
160
160
|
/** @type {Map<string, { total: number, chunks: Uint8Array[], seen: boolean[], bytes: number, firstSeenAt: number, lastSeenAt: number }>} */
|
|
161
161
|
const partials = new Map()
|
|
162
162
|
|
package/link/handshake.mjs
CHANGED
|
@@ -2,50 +2,66 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
5
6
|
import { keyPairFromSeed, pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
6
7
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
7
8
|
|
|
8
9
|
import { normalizeDtlsFingerprint } from './sdp_fingerprint.mjs'
|
|
9
10
|
|
|
11
|
+
/** advert / peer-hint 端口规范化(re-export,调用方可直接从 core/tcp_port 导入) */
|
|
12
|
+
export const normalizeAdvertTcpPort = normalizeTcpPort
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Link 握手签名域标识符。
|
|
12
16
|
*/
|
|
13
17
|
export const LINK_HANDSHAKE_DOMAIN = 'fount-link'
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* 规范化链路绑定材料:DTLS fingerprint 或 64-hex linkId。
|
|
21
|
+
* @param {unknown} value 原始 binding
|
|
22
|
+
* @returns {string | null} 规范化 binding,无效时 null
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeLinkBinding(value) {
|
|
25
|
+
const dtls = normalizeDtlsFingerprint(value)
|
|
26
|
+
if (dtls) return dtls
|
|
27
|
+
const hex = normalizeHex64(value)
|
|
28
|
+
return isHex64(hex) ? hex : null
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
/**
|
|
16
32
|
* 构造 link auth 待签名字节串。
|
|
17
33
|
* @param {string} peerNonce 对端 hello 中的 nonce(64 位 hex)
|
|
18
|
-
* @param {string}
|
|
34
|
+
* @param {string} localBinding 本地绑定材料(DTLS fingerprint 或 linkId)
|
|
19
35
|
* @param {string} localNodeHash 本地节点 nodeHash(64 位 hex)
|
|
20
36
|
* @returns {Uint8Array} 待签名消息字节
|
|
21
37
|
*/
|
|
22
|
-
export function buildAuthMessage(peerNonce,
|
|
38
|
+
export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
|
|
23
39
|
const nonce = normalizeHex64(peerNonce)
|
|
24
|
-
const
|
|
40
|
+
const binding = normalizeLinkBinding(localBinding)
|
|
25
41
|
const nodeHash = normalizeHex64(localNodeHash)
|
|
26
42
|
if (!/^[\da-f]{64}$/u.test(nonce))
|
|
27
43
|
throw new Error('p2p: auth nonce must be 64 hex characters')
|
|
28
|
-
if (!
|
|
29
|
-
throw new Error('p2p:
|
|
44
|
+
if (!binding)
|
|
45
|
+
throw new Error('p2p: link binding missing or invalid')
|
|
30
46
|
if (!isHex64(nodeHash))
|
|
31
47
|
throw new Error('p2p: nodeHash must be 64 hex characters')
|
|
32
|
-
return Buffer.from(`${LINK_HANDSHAKE_DOMAIN}\0${nonce}\0${
|
|
48
|
+
return Buffer.from(`${LINK_HANDSHAKE_DOMAIN}\0${nonce}\0${binding}\0${nodeHash}`, 'utf8')
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
/**
|
|
36
52
|
* 构造 link hello 握手包。
|
|
37
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [
|
|
53
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
|
|
38
54
|
* @returns {{ v: 1, nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
|
|
39
55
|
*/
|
|
40
|
-
export function buildHello(
|
|
56
|
+
export function buildHello(options = {}) {
|
|
41
57
|
let publicKey = null
|
|
42
|
-
if (!
|
|
58
|
+
if (!options.nodeHash || !options.nodePubKey) {
|
|
43
59
|
const derived = keyPairFromSeed(Buffer.from(ensureNodeSeed(), 'hex'))
|
|
44
60
|
publicKey = derived.publicKey
|
|
45
61
|
}
|
|
46
|
-
const nodeHash = normalizeHex64(
|
|
47
|
-
const nodePubKey = normalizeHex64(
|
|
48
|
-
const nonce = normalizeHex64(
|
|
62
|
+
const nodeHash = normalizeHex64(options.nodeHash || getNodeHash())
|
|
63
|
+
const nodePubKey = normalizeHex64(options.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
64
|
+
const nonce = normalizeHex64(options.nonce || randomBytes(32).toString('hex'))
|
|
49
65
|
if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce))
|
|
50
66
|
throw new Error('p2p: invalid hello fields')
|
|
51
67
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
@@ -56,19 +72,19 @@ export function buildHello(opts = {}) {
|
|
|
56
72
|
/**
|
|
57
73
|
* 对 link auth 消息签名。
|
|
58
74
|
* @param {string} peerNonce 对端 hello 中的 nonce
|
|
59
|
-
* @param {string}
|
|
60
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [
|
|
75
|
+
* @param {string} localBinding 本地绑定材料(DTLS fingerprint 或 linkId)
|
|
76
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string }} [options] 签名密钥与 nodeHash 覆盖
|
|
61
77
|
* @returns {Promise<{ sig: string }>} hex 签名
|
|
62
78
|
*/
|
|
63
|
-
export async function buildAuth(peerNonce,
|
|
64
|
-
const seed =
|
|
65
|
-
? Buffer.from(
|
|
79
|
+
export async function buildAuth(peerNonce, localBinding, options = {}) {
|
|
80
|
+
const seed = options.secretKey
|
|
81
|
+
? Buffer.from(options.secretKey)
|
|
66
82
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
67
83
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
68
|
-
const nodeHash = normalizeHex64(
|
|
84
|
+
const nodeHash = normalizeHex64(options.nodeHash || pubKeyHash(publicKey))
|
|
69
85
|
if (nodeHash !== pubKeyHash(publicKey))
|
|
70
86
|
throw new Error('p2p: auth nodeHash does not match secretKey')
|
|
71
|
-
const message = buildAuthMessage(peerNonce,
|
|
87
|
+
const message = buildAuthMessage(peerNonce, localBinding, nodeHash)
|
|
72
88
|
const signature = await sign(message, secretKey)
|
|
73
89
|
return { sig: Buffer.from(signature).toString('hex') }
|
|
74
90
|
}
|
|
@@ -98,18 +114,18 @@ export function parseHello(hello) {
|
|
|
98
114
|
* @param {unknown} hello 对端 hello
|
|
99
115
|
* @param {unknown} auth 对端 auth(含 sig)
|
|
100
116
|
* @param {string} expectedNonce 本地 hello 发出的 nonce
|
|
101
|
-
* @param {string}
|
|
117
|
+
* @param {string} remoteBinding 对端绑定材料(DTLS fingerprint 或 linkId)
|
|
102
118
|
* @returns {Promise<string | null>} 验证通过的 nodeHash,失败返回 null
|
|
103
119
|
*/
|
|
104
|
-
export async function verifyAuth(hello, auth, expectedNonce,
|
|
120
|
+
export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
105
121
|
const parsedHello = parseHello(hello)
|
|
106
122
|
if (!parsedHello) return null
|
|
107
123
|
const signatureHex = String(auth?.sig ?? '').trim().toLowerCase()
|
|
108
|
-
const
|
|
109
|
-
if (!/^[\da-f]{128}$/u.test(signatureHex) || !
|
|
124
|
+
const binding = normalizeLinkBinding(remoteBinding)
|
|
125
|
+
if (!/^[\da-f]{128}$/u.test(signatureHex) || !binding) return null
|
|
110
126
|
const normalizedNonce = normalizeHex64(expectedNonce)
|
|
111
127
|
if (!isHex64(normalizedNonce)) return null
|
|
112
|
-
const message = buildAuthMessage(normalizedNonce,
|
|
128
|
+
const message = buildAuthMessage(normalizedNonce, binding, parsedHello.nodeHash)
|
|
113
129
|
const ok = await verify(
|
|
114
130
|
Buffer.from(signatureHex, 'hex'),
|
|
115
131
|
message,
|
|
@@ -123,36 +139,44 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteFingerprintFr
|
|
|
123
139
|
* @param {string} topic 广播主题
|
|
124
140
|
* @param {number} ts 时间戳(毫秒)
|
|
125
141
|
* @param {string} nodeHash 节点 nodeHash
|
|
142
|
+
* @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
|
|
126
143
|
* @returns {Uint8Array} 待签名消息字节
|
|
127
144
|
*/
|
|
128
|
-
export function buildAdvertMessage(topic, ts, nodeHash) {
|
|
129
|
-
|
|
145
|
+
export function buildAdvertMessage(topic, ts, nodeHash, tcpPort = null) {
|
|
146
|
+
const base = `fount-advert\0${String(topic)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
|
|
147
|
+
const port = normalizeTcpPort(tcpPort)
|
|
148
|
+
return Buffer.from(port == null ? base : `${base}\0${port}`, 'utf8')
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
/**
|
|
133
152
|
* 构造带签名的 discovery advert。
|
|
134
153
|
* @param {string} topic 广播主题
|
|
135
154
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
136
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string } | null} [
|
|
137
|
-
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string }>} 签名 advert
|
|
155
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number } | null} [options] 签名身份与可选 tcpPort
|
|
156
|
+
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
|
|
138
157
|
*/
|
|
139
|
-
export async function buildSignedAdvert(topic, ts = Date.now(),
|
|
140
|
-
const seed =
|
|
141
|
-
? Buffer.from(
|
|
158
|
+
export async function buildSignedAdvert(topic, ts = Date.now(), options = null) {
|
|
159
|
+
const seed = options?.secretKey
|
|
160
|
+
? Buffer.from(options.secretKey)
|
|
142
161
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
143
162
|
const { publicKey, secretKey } = keyPairFromSeed(seed)
|
|
144
|
-
const nodeHash = normalizeHex64(
|
|
145
|
-
const nodePubKey = normalizeHex64(
|
|
163
|
+
const nodeHash = normalizeHex64(options?.nodeHash || pubKeyHash(publicKey))
|
|
164
|
+
const nodePubKey = normalizeHex64(options?.nodePubKey || Buffer.from(publicKey).toString('hex'))
|
|
146
165
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
147
166
|
throw new Error('p2p: advert nodePubKey does not match nodeHash')
|
|
148
|
-
const
|
|
167
|
+
const tcpPort = normalizeTcpPort(options?.tcpPort)
|
|
168
|
+
if (options?.tcpPort != null && options.tcpPort !== '' && tcpPort == null)
|
|
169
|
+
throw new Error('p2p: advert tcpPort invalid')
|
|
170
|
+
const message = buildAdvertMessage(topic, ts, nodeHash, tcpPort)
|
|
149
171
|
const sig = await sign(message, secretKey)
|
|
150
|
-
|
|
172
|
+
const advert = {
|
|
151
173
|
nodeHash,
|
|
152
174
|
nodePubKey,
|
|
153
175
|
ts,
|
|
154
176
|
sig: Buffer.from(sig).toString('hex'),
|
|
155
177
|
}
|
|
178
|
+
if (tcpPort != null) advert.tcpPort = tcpPort
|
|
179
|
+
return advert
|
|
156
180
|
}
|
|
157
181
|
|
|
158
182
|
/**
|
|
@@ -169,7 +193,10 @@ export async function verifySignedAdvert(topic, advert, now = Date.now(), maxSke
|
|
|
169
193
|
const ts = Number(advert?.ts)
|
|
170
194
|
const sig = String(advert?.sig ?? '').trim().toLowerCase()
|
|
171
195
|
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewMs || !/^[\da-f]{128}$/u.test(sig)) return null
|
|
172
|
-
const
|
|
196
|
+
const hasTcpPortField = advert?.tcpPort != null && advert.tcpPort !== ''
|
|
197
|
+
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
198
|
+
if (hasTcpPortField && tcpPort == null) return null
|
|
199
|
+
const message = buildAdvertMessage(topic, ts, parsedHello.nodeHash, tcpPort)
|
|
173
200
|
const ok = await verify(Buffer.from(sig, 'hex'), message, Buffer.from(parsedHello.nodePubKey, 'hex'))
|
|
174
201
|
return ok ? parsedHello.nodeHash : null
|
|
175
202
|
}
|