@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.
Files changed (47) hide show
  1. package/core/tcp_port.mjs +11 -0
  2. package/dag/canonicalize_row.mjs +5 -5
  3. package/discovery/advert_peer_hints.mjs +21 -0
  4. package/discovery/{bt.mjs → bt/index.mjs} +151 -55
  5. package/discovery/bt/peer_hints.mjs +41 -0
  6. package/discovery/bt/runtime.mjs +46 -0
  7. package/discovery/lan_peer_hints.mjs +43 -0
  8. package/discovery/mdns.mjs +10 -6
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/topo_order_memo.mjs +3 -3
  14. package/files/chunk_responder.mjs +1 -1
  15. package/files/evfs.mjs +26 -26
  16. package/files/transfer_key.mjs +9 -9
  17. package/files/transfer_key_registry.mjs +9 -9
  18. package/governance/join_pow.mjs +17 -17
  19. package/index.mjs +3 -2
  20. package/link/channel_mux.mjs +7 -7
  21. package/link/frame.mjs +5 -5
  22. package/link/handshake.mjs +63 -36
  23. package/link/pipe.mjs +404 -0
  24. package/link/providers/ble_gatt.mjs +339 -0
  25. package/link/providers/index.mjs +90 -0
  26. package/link/providers/lan_tcp.mjs +342 -0
  27. package/link/providers/levels.mjs +9 -0
  28. package/link/providers/webrtc.mjs +385 -0
  29. package/mailbox/deliver_or_store.mjs +13 -13
  30. package/mailbox/wire.mjs +4 -4
  31. package/node/reputation_store.mjs +5 -5
  32. package/node/retention_policy.mjs +8 -8
  33. package/overlay/index.mjs +6 -6
  34. package/package.json +3 -2
  35. package/registries/inbound.mjs +8 -8
  36. package/rooms/scoped_link.mjs +23 -21
  37. package/timeline/append_core.mjs +3 -3
  38. package/transport/group_link_set.mjs +35 -33
  39. package/transport/link_registry.mjs +250 -63
  40. package/transport/peer_pool.mjs +4 -4
  41. package/transport/user_room.mjs +12 -14
  42. package/utils/ttl_map.mjs +41 -0
  43. package/wire/group_part.mjs +3 -3
  44. package/wire/part_ingress.mjs +14 -14
  45. package/wire/part_query.mjs +81 -82
  46. package/wire/part_query_cache.mjs +7 -0
  47. 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, resolveTransferKeyDeps } from './transfer_key_registry.mjs'
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 transferKeyDepsForReplica(replicaUsername, manifest) {
21
- const rawDeps = resolveTransferKeyDeps(undefined, manifest)
20
+ function transferKeyDependenciesForReplica(replicaUsername, manifest) {
21
+ const rawDependencies = resolveTransferKeyDependencies(undefined, manifest)
22
22
  return {
23
- getGroupFileMasterKey: rawDeps.getGroupFileMasterKey
24
- ? (groupId, keyGeneration) => rawDeps.getGroupFileMasterKey(replicaUsername, groupId, keyGeneration)
23
+ getGroupFileMasterKey: rawDependencies.getGroupFileMasterKey
24
+ ? (groupId, keyGeneration) => rawDependencies.getGroupFileMasterKey(replicaUsername, groupId, keyGeneration)
25
25
  : undefined,
26
- getVaultMasterKey: rawDeps.getVaultMasterKey
27
- ? entityHash => rawDeps.getVaultMasterKey(replicaUsername, 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 }} [opts] miss 拉取
35
+ * @param {{ fetchChunk?: Function }} [options] miss 拉取
36
36
  * @returns {Promise<boolean>} 全部 part 是否已就位
37
37
  */
38
- async function ensureManifestPartsLocal(username, manifest, opts = {}) {
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 (opts.fetchChunk || fetchChunk)({
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 }} [opts] miss 拉取
84
+ * @param {{ username?: string, fetchChunk?: Function }} [options] miss 拉取
85
85
  * @returns {Promise<Buffer | null>} 明文内容
86
86
  */
87
- export async function readManifestPlaintext(replicaUsername, manifest, opts = {}) {
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 = opts.username || replicaUsername
95
- if (!await ensureManifestPartsLocal(username, manifest, opts)) return null
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, transferKeyDepsForReplica(replicaUsername, manifest))
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 }} [opts] miss 拉取
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, opts = {}) {
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, opts)
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 = opts.username || replicaUsername
120
- if (!await ensureManifestPartsLocal(username, manifest, opts)) return null
119
+ const username = options.username || replicaUsername
120
+ if (!await ensureManifestPartsLocal(username, manifest, options)) return null
121
121
 
122
- const deps = transferKeyDepsForReplica(replicaUsername, manifest)
123
- const contentKey = await resolveContentKey(manifest.transferKeyDescriptor, manifest, deps)
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 }} [opts] miss 拉取
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, opts = {}) {
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: opts.username || replicaUsername,
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, opts)
233
+ return readManifestPlaintext(replicaUsername, manifest, options)
234
234
  }
@@ -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> }} deps 密钥源
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, deps = {}) {
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 || !deps.getGroupFileMasterKey) return null
29
- const groupKey = await deps.getGroupFileMasterKey(String(groupId), descriptor.keyGeneration)
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 || !deps.getVaultMasterKey) return null
37
- const vaultKey = await deps.getVaultMasterKey(String(entityHash))
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 }} deps 密钥源
73
+ * @param {{ getGroupFileMasterKey?: Function, getVaultMasterKey?: Function }} dependencies 密钥源
74
74
  * @returns {Promise<Buffer | null>} 完整明文
75
75
  */
76
- export async function assembleManifestPlaintext(manifest, partBytes, deps = {}) {
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, deps)
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 transferDepsByOwner = new Map()
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> }} deps 密钥源
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 registerTransferKeyDeps(ownerId, deps) {
12
+ export function registerTransferKeyDependencies(ownerId, dependencies) {
13
13
  const key = String(ownerId)
14
- const prev = transferDepsByOwner.get(key) || {}
15
- transferDepsByOwner.set(key, { ...prev, ...deps })
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 unregisterTransferKeyDeps(ownerId) {
53
+ export function unregisterTransferKeyDependencies(ownerId) {
54
54
  const key = String(ownerId)
55
- transferDepsByOwner.delete(key)
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 resolveTransferKeyDeps(ownerId, manifest) {
76
+ export function resolveTransferKeyDependencies(ownerId, manifest) {
77
77
  const resolved = ownerId || (manifest ? resolveManifestTransferOwnerId(manifest) : null)
78
- if (resolved) return transferDepsByOwner.get(String(resolved)) || {}
78
+ if (resolved) return transferDependenciesByOwner.get(String(resolved)) || {}
79
79
  return {}
80
80
  }
81
81
 
@@ -85,40 +85,40 @@ export function powVoluntaryBonus(achievedBits, floorBits, tunables = admissionT
85
85
 
86
86
  /**
87
87
  * @param {object} powSolution 客户端 solution
88
- * @param {object} opts 校验上下文
89
- * @param {string} opts.groupId 群 ID
90
- * @param {string} opts.senderNodeHash 签名者 nodeHash(须与 joiner 绑定)
91
- * @param {string[]} opts.knownAnchors 近期 tip / checkpoint root 列表
92
- * @param {number} [opts.now=Date.now()] 当前时间
93
- * @param {number} [opts.difficultyBits=0] 准入 floor(前导零 bit)
94
- * @param {number} [opts.epochMs=JOIN_POW_DEFAULT_EPOCH_MS] epoch 长度
95
- * @param {number} [opts.epochSkew=JOIN_POW_DEFAULT_EPOCH_SKEW] 允许 epoch 偏移
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, opts) {
99
- const floorBits = Math.max(0, Math.floor(Number(opts.difficultyBits) || 0))
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 ?? opts.senderNodeHash ?? '').trim().toLowerCase()
107
- const senderNodeHash = String(opts.senderNodeHash ?? '').trim().toLowerCase()
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 = (opts.knownAnchors ?? []).map(a => String(a).trim()).filter(Boolean)
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(opts.epochMs) || JOIN_POW_DEFAULT_EPOCH_MS)
116
- const skew = Math.max(0, Math.floor(Number(opts.epochSkew) ?? JOIN_POW_DEFAULT_EPOCH_SKEW))
117
- const nowEpoch = Math.floor((opts.now ?? Date.now()) / epochMs)
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: opts.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
- * 重型子系统请直接从子路径导入(如 `./dag/index.mjs`)。
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'
@@ -110,15 +110,15 @@ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CH
110
110
  /**
111
111
  * 订阅 bufferedamountlow 事件,返回取消订阅函数。
112
112
  * @param {RTCDataChannel} channel RTC 数据通道
113
- * @param {() => void} cb 低水位回调
113
+ * @param {() => void} callback 低水位回调
114
114
  * @returns {() => void} 取消订阅函数
115
115
  */
116
- export function onBufferedAmountLow(channel, cb) {
116
+ export function onBufferedAmountLow(channel, callback) {
117
117
  /**
118
118
  * bufferedamountlow 事件处理函数。
119
119
  * @returns {void}
120
120
  */
121
- const handler = () => cb()
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 }} opts 通道访问与高水位配置
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(opts) {
138
- const { getChannel } = opts
139
- const highWatermarkBytes = Math.max(CHANNEL_LOW_THRESHOLD_BYTES, Number(opts.highWatermarkBytes) || CHANNEL_HIGH_WATERMARK_BYTES)
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 }} [opts] 大小、并发分片数与超时配置
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(opts = {}) {
157
- const maxMessageBytes = Math.max(1024, Number(opts.maxMessageBytes) || DEFAULT_MAX_MESSAGE_BYTES)
158
- const maxPartials = Math.max(1, Number(opts.maxPartials) || DEFAULT_MAX_PARTIAL_MESSAGES)
159
- const partialTimeoutMs = Math.max(1000, Number(opts.partialTimeoutMs) || DEFAULT_PARTIAL_TIMEOUT_MS)
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
 
@@ -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} localFingerprint 本地 DTLS fingerprint
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, localFingerprint, localNodeHash) {
38
+ export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
23
39
  const nonce = normalizeHex64(peerNonce)
24
- const fingerprint = normalizeDtlsFingerprint(localFingerprint)
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 (!fingerprint)
29
- throw new Error('p2p: DTLS fingerprint missing or invalid')
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${fingerprint}\0${nodeHash}`, 'utf8')
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 }} [opts] 可选身份字段,省略则从本地节点种子推导
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(opts = {}) {
56
+ export function buildHello(options = {}) {
41
57
  let publicKey = null
42
- if (!opts.nodeHash || !opts.nodePubKey) {
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(opts.nodeHash || getNodeHash())
47
- const nodePubKey = normalizeHex64(opts.nodePubKey || Buffer.from(publicKey).toString('hex'))
48
- const nonce = normalizeHex64(opts.nonce || randomBytes(32).toString('hex'))
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} localFingerprint 本地 DTLS fingerprint
60
- * @param {{ secretKey?: Uint8Array, nodeHash?: string }} [opts] 签名密钥与 nodeHash 覆盖
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, localFingerprint, opts = {}) {
64
- const seed = opts.secretKey
65
- ? Buffer.from(opts.secretKey)
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(opts.nodeHash || pubKeyHash(publicKey))
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, localFingerprint, nodeHash)
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} remoteFingerprintFromSdp SDP 提取的远端 DTLS fingerprint
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, remoteFingerprintFromSdp) {
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 fingerprint = normalizeDtlsFingerprint(remoteFingerprintFromSdp)
109
- if (!/^[\da-f]{128}$/u.test(signatureHex) || !fingerprint) return null
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, fingerprint, parsedHello.nodeHash)
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
- return Buffer.from(`fount-advert\0${String(topic)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`, 'utf8')
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} [opts] 签名身份,省略则用本地节点
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(), opts = null) {
140
- const seed = opts?.secretKey
141
- ? Buffer.from(opts.secretKey)
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(opts?.nodeHash || pubKeyHash(publicKey))
145
- const nodePubKey = normalizeHex64(opts?.nodePubKey || Buffer.from(publicKey).toString('hex'))
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 message = buildAdvertMessage(topic, ts, nodeHash)
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
- return {
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 message = buildAdvertMessage(topic, ts, parsedHello.nodeHash)
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
  }