@steve02081504/fount-p2p 0.0.6 → 0.0.8

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.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 规范化 TCP 端口(advert / peer hint 共用)。
3
+ * @param {unknown} port 原始端口
4
+ * @returns {number | null} 有效端口或 null(未提供 / 非法)
5
+ */
6
+ export function normalizeTcpPort(port) {
7
+ if (port == null || port === '') return null
8
+ const value = Number(port)
9
+ if (!Number.isInteger(value) || value < 1 || value > 65535) return null
10
+ return value
11
+ }
@@ -0,0 +1,21 @@
1
+ import { normalizeTcpPort } from '../core/tcp_port.mjs'
2
+
3
+ import { noteBtPeerHint } from './bt/peer_hints.mjs'
4
+ import { noteLanPeerHint } from './lan_peer_hints.mjs'
5
+
6
+ /**
7
+ * 从已验证的 discovery advert + provider meta 写入 LAN/BT peer hints。
8
+ * 任意 discovery 路径(node / group / scoped topic)收到 advert 时都应调用。
9
+ * @param {string} verifiedNodeHash 验签通过的 nodeHash
10
+ * @param {{ tcpPort?: unknown } | null | undefined} body advert body
11
+ * @param {{ address?: unknown, peripheralId?: unknown } | null | undefined} meta discovery provider meta
12
+ * @returns {void}
13
+ */
14
+ export function noteAdvertPeerHints(verifiedNodeHash, body, meta) {
15
+ if (meta?.peripheralId)
16
+ noteBtPeerHint(verifiedNodeHash, meta.peripheralId)
17
+ const tcpPort = normalizeTcpPort(body?.tcpPort)
18
+ const address = String(meta?.address || '').trim()
19
+ if (tcpPort != null && address)
20
+ noteLanPeerHint(verifiedNodeHash, { host: address, port: tcpPort })
21
+ }
@@ -1,56 +1,19 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import process from 'node:process'
2
+
3
+ import { getBtPeerHint } from './peer_hints.mjs'
4
+ import { loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
5
+
6
+ /** 重导出 waitPoweredOn,供 discovery 调用方使用。 */
7
+ export { waitPoweredOn } from './runtime.mjs'
3
8
 
4
9
  const BT_SERVICE_UUID = 'f017f017f017f017f017f017f017f017'
5
10
  const BT_CHARACTERISTIC_UUID = 'f017f017f017f017f017f017f017f018'
11
+ const BT_SIGNAL_CHAR_UUID = 'f017f017f017f017f017f017f017f01b'
6
12
  const BT_DEVICE_NAME = 'fount-bt'
7
13
  const MAX_ADVERT_BLOB_BYTES = 12 * 1024
14
+ const MAX_SIGNAL_BLOB_BYTES = 8 * 1024
8
15
  const PERIPHERAL_RESCAN_MS = 15_000
9
16
 
10
- /**
11
- * 解析 Bluetooth 发现角色(scan / dual)。
12
- * @returns {'scan' | 'dual'} 当前平台或环境变量指定的角色
13
- */
14
- function resolveBtRole() {
15
- const override = String(process.env.FOUNT_BT_DISCOVERY_ROLE || '').trim().toLowerCase()
16
- if (override === 'dual') return 'dual'
17
- if (override === 'scan') return 'scan'
18
- return process.platform === 'win32' ? 'scan' : 'dual'
19
- }
20
-
21
- /**
22
- * 加载 Noble BLE central 库。
23
- * @returns {Promise<any>} Noble 运行时实例
24
- */
25
- async function loadNoble() {
26
- const mod = await import('@stoprocent/noble')
27
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
28
- return mod.default ?? mod
29
- }
30
-
31
- /**
32
- * 加载 Bleno BLE peripheral 库。
33
- * @returns {Promise<any>} Bleno 运行时实例
34
- */
35
- async function loadBleno() {
36
- const mod = await import('@stoprocent/bleno')
37
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
38
- return mod.default ?? mod
39
- }
40
-
41
- /**
42
- * 等待 BLE 运行时进入 poweredOn(兼容 noble v1/v2 与 bleno)。
43
- * @param {*} runtime noble 或 bleno 实例
44
- * @param {number} [timeout] 超时毫秒
45
- * @returns {Promise<void>}
46
- */
47
- export async function waitPoweredOn(runtime, timeout) {
48
- const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
49
- if (typeof wait !== 'function')
50
- throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
51
- return wait.call(runtime, timeout)
52
- }
53
-
54
17
  /**
55
18
  * 探测本机 noble 运行时是否具备 BT 扫描所需 API。
56
19
  * @returns {Promise<boolean>} noble 可加载且具备 startScanningAsync 与 waitForPoweredOn(Async) 时为 true
@@ -125,6 +88,7 @@ function addListener(bucket, topic, listener) {
125
88
  * - 默认在 Windows 上只启用 scan 侧发现(单适配器 central+peripheral 常冲突)
126
89
  * - 其他平台默认 dual:advertise + scan
127
90
  * - 通过固定 BLE service + read characteristic 传输完整 advert 列表,避免 31-byte 广告包限制
91
+ * - dual 下额外暴露 write characteristic 传短信令;central 可按 peer hint 写信令
128
92
  *
129
93
  * @returns {import('./index.mjs').DiscoveryProvider} Bluetooth 发现提供者
130
94
  */
@@ -134,6 +98,8 @@ export function createBluetoothDiscoveryProvider() {
134
98
  const adverts = new Map()
135
99
  /** @type {Map<string, Set<Function>>} */
136
100
  const advertListeners = new Map()
101
+ /** @type {Map<string, Set<Function>>} */
102
+ const signalListeners = new Map()
137
103
  /** @type {Map<string, number>} */
138
104
  const inspectedAt = new Map()
139
105
  let nobleRuntime = null
@@ -149,17 +115,17 @@ export function createBluetoothDiscoveryProvider() {
149
115
  if (role === 'scan') return null
150
116
  if (blenoRuntime) return blenoRuntime
151
117
  const bleno = await loadBleno()
152
- const characteristic = new bleno.Characteristic({
118
+ const advertCharacteristic = new bleno.Characteristic({
153
119
  uuid: BT_CHARACTERISTIC_UUID,
154
120
  properties: ['read'],
155
121
  /**
156
- * BLE characteristic 读请求回调。
157
- * @param {*} _handle Bleno handle(未使用)
158
- * @param {number} offset 读取偏移
159
- * @param {Function} callback Bleno 结果回调
122
+ * stoprocent/bleno onReadRequest(connection, offset, callback)
123
+ * @param {*} _connection 连接句柄
124
+ * @param {number} offset 偏移
125
+ * @param {Function} callback 结果
160
126
  * @returns {void}
161
127
  */
162
- onReadRequest(_handle, offset, callback) {
128
+ onReadRequest(_connection, offset, callback) {
163
129
  try {
164
130
  const blob = serializeAdvertBlob(adverts)
165
131
  if (offset > blob.length) {
@@ -173,11 +139,39 @@ export function createBluetoothDiscoveryProvider() {
173
139
  }
174
140
  },
175
141
  })
142
+ const signalCharacteristic = new bleno.Characteristic({
143
+ uuid: BT_SIGNAL_CHAR_UUID,
144
+ properties: ['write', 'writeWithoutResponse'],
145
+ /**
146
+ * stoprocent/bleno onWriteRequest(connection, data, offset, withoutResponse, callback)
147
+ * @param {*} _connection 连接句柄
148
+ * @param {Buffer} data 信令 blob
149
+ * @param {number} _offset 偏移
150
+ * @param {boolean} _withoutResponse 无响应写
151
+ * @param {Function} callback 结果
152
+ * @returns {void}
153
+ */
154
+ onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
155
+ try {
156
+ const parsed = JSON.parse(Buffer.from(data).toString('utf8'))
157
+ const topic = String(parsed?.topic || '')
158
+ const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
159
+ if (topic && bytes.byteLength)
160
+ for (const listener of signalListeners.get(topic) || [])
161
+ listener(bytes, { provider: 'bt' })
162
+
163
+ callback(bleno.Characteristic.RESULT_SUCCESS)
164
+ }
165
+ catch {
166
+ callback(bleno.Characteristic.RESULT_UNLIKELY_ERROR)
167
+ }
168
+ },
169
+ })
176
170
  await waitPoweredOn(bleno, 5_000)
177
171
  await bleno.setServicesAsync([
178
172
  new bleno.PrimaryService({
179
173
  uuid: BT_SERVICE_UUID,
180
- characteristics: [characteristic],
174
+ characteristics: [advertCharacteristic, signalCharacteristic],
181
175
  }),
182
176
  ])
183
177
  blenoRuntime = bleno
@@ -192,14 +186,14 @@ export function createBluetoothDiscoveryProvider() {
192
186
  if (role === 'scan') return
193
187
  const bleno = await ensurePeripheralRuntime()
194
188
  if (!bleno) return
195
- if (!adverts.size) {
189
+ if (!adverts.size && !signalListeners.size) {
196
190
  if (advertisingStarted) {
197
191
  await bleno.stopAdvertisingAsync().catch(() => { })
198
192
  advertisingStarted = false
199
193
  }
200
194
  return
201
195
  }
202
- serializeAdvertBlob(adverts)
196
+ if (adverts.size) serializeAdvertBlob(adverts)
203
197
  if (!advertisingStarted) {
204
198
  await bleno.startAdvertisingAsync(BT_DEVICE_NAME, [BT_SERVICE_UUID])
205
199
  advertisingStarted = true
@@ -256,10 +250,94 @@ export function createBluetoothDiscoveryProvider() {
256
250
  scanningStarted = true
257
251
  }
258
252
 
253
+ /**
254
+ * Central:按 peer hint 连接并对端 signal characteristic 写短包。
255
+ * @param {string} topic 信令 topic
256
+ * @param {string} to 目标 nodeHash
257
+ * @param {Uint8Array} bytes 载荷
258
+ * @returns {Promise<void>}
259
+ */
260
+ async function sendSignalViaGatt(topic, to, bytes) {
261
+ const hint = getBtPeerHint(to)
262
+ if (!hint) throw new Error('p2p: bt signal missing peer hint')
263
+ const blob = Buffer.from(JSON.stringify({
264
+ topic: String(topic),
265
+ to: String(to),
266
+ data: Buffer.from(bytes).toString('base64'),
267
+ }), 'utf8')
268
+ if (blob.byteLength > MAX_SIGNAL_BLOB_BYTES)
269
+ throw new Error(`p2p: bt signal blob exceeds ${MAX_SIGNAL_BLOB_BYTES} bytes`)
270
+ await ensureScanRuntime()
271
+ const noble = nobleRuntime
272
+ const wantId = hint.peripheralId
273
+ /**
274
+ * 先查 noble 已缓存的 peripheral,避免只等下一次 advertise 漏报。
275
+ * @returns {*|null} 已缓存的 peripheral,未命中为 null
276
+ */
277
+ function cachedPeripheral() {
278
+ const table = noble?._peripherals
279
+ if (!table || typeof table !== 'object') return null
280
+ for (const found of Object.values(table)) {
281
+ const id = String(found?.id || found?.address || '')
282
+ if (id === wantId) return found
283
+ }
284
+ return null
285
+ }
286
+ const peripheral = cachedPeripheral() ?? await new Promise((resolve, reject) => {
287
+ const deadline = setTimeout(() => {
288
+ cleanup()
289
+ reject(new Error('p2p: bt signal peripheral timeout'))
290
+ }, 8_000)
291
+ /**
292
+ * @returns {void}
293
+ */
294
+ function cleanup() {
295
+ clearTimeout(deadline)
296
+ noble.removeListener('discover', onDiscover)
297
+ }
298
+ /**
299
+ * @param {*} found peripheral
300
+ * @returns {void}
301
+ */
302
+ function onDiscover(found) {
303
+ const id = String(found?.id || found?.address || '')
304
+ if (id !== wantId) return
305
+ cleanup()
306
+ resolve(found)
307
+ }
308
+ noble.on('discover', onDiscover)
309
+ const again = cachedPeripheral()
310
+ if (again) {
311
+ cleanup()
312
+ resolve(again)
313
+ }
314
+ })
315
+ try {
316
+ await peripheral.connectAsync()
317
+ const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
318
+ [BT_SERVICE_UUID],
319
+ [BT_SIGNAL_CHAR_UUID],
320
+ )
321
+ if (!characteristics?.length) throw new Error('p2p: bt signal characteristic missing')
322
+ await characteristics[0].writeAsync(blob, false)
323
+ }
324
+ finally {
325
+ try { await peripheral.disconnectAsync() } catch { /* ignore */ }
326
+ }
327
+ }
328
+
259
329
  return {
260
330
  id: 'bt',
261
331
  priority: 20,
262
- caps: { canDiscover: true, canSignal: false, canRelay: false },
332
+ caps: { canDiscover: true, canSignal: true, canRelay: false },
333
+ /**
334
+ * 是否具备对该 peer 的 GATT 信令能力(需未过期 BT peer hint)。
335
+ * @param {string} to 目标 nodeHash
336
+ * @returns {boolean} 有 hint 则可尝试
337
+ */
338
+ canSignalTo(to) {
339
+ return !!getBtPeerHint(to)
340
+ },
263
341
  /**
264
342
  * 广播指定 topic 的 advert。
265
343
  * @param {string} topic advert 主题
@@ -285,5 +363,31 @@ export function createBluetoothDiscoveryProvider() {
285
363
  await ensureScanRuntime()
286
364
  return addListener(advertListeners, String(topic), onAdvert)
287
365
  },
366
+ /**
367
+ * 经 GATT 向近场 peer 发送信令。
368
+ * @param {string} topic 信令 topic
369
+ * @param {string} to 目标 nodeHash
370
+ * @param {Uint8Array} bytes 载荷
371
+ * @returns {Promise<void>}
372
+ */
373
+ async sendSignal(topic, to, bytes) {
374
+ await sendSignalViaGatt(topic, to, bytes)
375
+ },
376
+ /**
377
+ * 监听经本机 peripheral signal characteristic 写入的信令。
378
+ * @param {string} topic 信令 topic
379
+ * @param {Function} onSignal 回调
380
+ * @returns {Promise<() => void>} 取消订阅
381
+ */
382
+ async onSignal(topic, onSignal) {
383
+ if (role === 'scan') return () => { }
384
+ await ensurePeripheralRuntime()
385
+ await refreshAdvertising()
386
+ const stop = addListener(signalListeners, String(topic), onSignal)
387
+ return () => {
388
+ stop()
389
+ void refreshAdvertising().catch(() => { })
390
+ }
391
+ },
288
392
  }
289
393
  }
@@ -0,0 +1,41 @@
1
+ import { normalizeHex64 } from '../../core/hexIds.mjs'
2
+ import { createTtlMap } from '../../utils/ttl_map.mjs'
3
+
4
+ /** BT peer hint 存活时间。 */
5
+ export const BT_PEER_HINT_TTL_MS = 5 * 60_000
6
+
7
+ /** @type {ReturnType<typeof createTtlMap<{ peripheralId: string }>>} */
8
+ const hints = createTtlMap(BT_PEER_HINT_TTL_MS)
9
+
10
+ /**
11
+ * 记录近场 BT 扫描到的 nodeHash → peripheral 映射。
12
+ * @param {string} nodeHash 节点 64 hex
13
+ * @param {string} peripheralId noble peripheral id / address
14
+ * @returns {void}
15
+ */
16
+ export function noteBtPeerHint(nodeHash, peripheralId) {
17
+ const hash = normalizeHex64(nodeHash)
18
+ const id = String(peripheralId || '').trim()
19
+ if (!hash || !id) return
20
+ hints.set(hash, { peripheralId: id })
21
+ }
22
+
23
+ /**
24
+ * 查询未过期的 BT peer hint。
25
+ * @param {string} nodeHash 节点 64 hex
26
+ * @param {number} [now=Date.now()] 当前时间(测试可注入)
27
+ * @returns {{ peripheralId: string } | null} hint 或 null
28
+ */
29
+ export function getBtPeerHint(nodeHash, now = Date.now()) {
30
+ const hash = normalizeHex64(nodeHash)
31
+ if (!hash) return null
32
+ return hints.get(hash, now)
33
+ }
34
+
35
+ /**
36
+ * 清空全部 BT peer hints(测试用)。
37
+ * @returns {void}
38
+ */
39
+ export function clearBtPeerHints() {
40
+ hints.clear()
41
+ }
@@ -0,0 +1,46 @@
1
+ import process from 'node:process'
2
+
3
+ /**
4
+ * 解析 Bluetooth 角色(scan / dual)。
5
+ * Win32 默认 scan(单适配器 central+peripheral 常冲突);可用 FOUNT_BT_DISCOVERY_ROLE 覆盖。
6
+ * @returns {'scan' | 'dual'} 生效角色
7
+ */
8
+ export function resolveBtRole() {
9
+ const override = String(process.env.FOUNT_BT_DISCOVERY_ROLE || '').trim().toLowerCase()
10
+ if (override === 'dual') return 'dual'
11
+ if (override === 'scan') return 'scan'
12
+ return process.platform === 'win32' ? 'scan' : 'dual'
13
+ }
14
+
15
+ /**
16
+ * 加载 Noble BLE central。
17
+ * @returns {Promise<any>} noble 运行时
18
+ */
19
+ export async function loadNoble() {
20
+ const mod = await import('@stoprocent/noble')
21
+ if (typeof mod.withBindings === 'function') return mod.withBindings('default')
22
+ return mod.default ?? mod
23
+ }
24
+
25
+ /**
26
+ * 加载 Bleno BLE peripheral。
27
+ * @returns {Promise<any>} bleno 运行时
28
+ */
29
+ export async function loadBleno() {
30
+ const mod = await import('@stoprocent/bleno')
31
+ if (typeof mod.withBindings === 'function') return mod.withBindings('default')
32
+ return mod.default ?? mod
33
+ }
34
+
35
+ /**
36
+ * 等待 BLE 运行时 poweredOn(兼容 noble/bleno v1/v2)。
37
+ * @param {*} runtime noble 或 bleno
38
+ * @param {number} [timeout] 超时毫秒
39
+ * @returns {Promise<void>}
40
+ */
41
+ export async function waitPoweredOn(runtime, timeout) {
42
+ const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
43
+ if (typeof wait !== 'function')
44
+ throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
45
+ return wait.call(runtime, timeout)
46
+ }
@@ -1,4 +1,4 @@
1
- /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => void | Promise<void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
1
+ /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, canSignalTo?: (to: string) => boolean, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => void | Promise<void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
2
2
 
3
3
  /** @type {Map<string, DiscoveryProvider>} */
4
4
  const providers = new Map()
@@ -95,15 +95,14 @@ export async function sendSignal(topic, to, bytes) {
95
95
  if (!capable.length) throw new Error('p2p: no discovery provider can signal')
96
96
  let sent = false
97
97
  let lastError = null
98
- for (const provider of capable)
99
- try {
100
- await Promise.resolve(provider.sendSignal(topic, to, bytes))
101
- sent = true
102
- }
103
- catch (error) {
104
- lastError = error
105
- console.warn(`p2p: discovery signal failed for ${provider.id}`, error)
106
- }
98
+ for (const provider of capable) if (provider?.canSignalTo?.(to)) try {
99
+ await Promise.resolve(provider.sendSignal(topic, to, bytes))
100
+ sent = true
101
+ }
102
+ catch (error) {
103
+ lastError = error
104
+ console.warn(`p2p: discovery signal failed for ${provider.id}`, error)
105
+ }
107
106
  if (!sent) throw lastError || new Error('p2p: no discovery provider delivered signal')
108
107
  }
109
108
 
@@ -0,0 +1,43 @@
1
+ import { normalizeHex64 } from '../core/hexIds.mjs'
2
+ import { normalizeTcpPort } from '../core/tcp_port.mjs'
3
+ import { createTtlMap } from '../utils/ttl_map.mjs'
4
+
5
+ /** LAN peer hint 存活时间。 */
6
+ export const LAN_PEER_HINT_TTL_MS = 5 * 60_000
7
+
8
+ /** @type {ReturnType<typeof createTtlMap<{ host: string, port: number }>>} */
9
+ const hints = createTtlMap(LAN_PEER_HINT_TTL_MS)
10
+
11
+ /**
12
+ * 记录 LAN 上观察到的 nodeHash → host:port。
13
+ * @param {string} nodeHash 节点 64 hex
14
+ * @param {{ host: string, port: number }} endpoint 端点
15
+ * @returns {void}
16
+ */
17
+ export function noteLanPeerHint(nodeHash, endpoint) {
18
+ const hash = normalizeHex64(nodeHash)
19
+ const host = String(endpoint?.host || '').trim()
20
+ const port = normalizeTcpPort(endpoint?.port)
21
+ if (!hash || !host || !port) return
22
+ hints.set(hash, { host, port })
23
+ }
24
+
25
+ /**
26
+ * 查询未过期的 LAN peer hint。
27
+ * @param {string} nodeHash 节点 64 hex
28
+ * @param {number} [now=Date.now()] 当前时间(测试可注入)
29
+ * @returns {{ host: string, port: number } | null} hint 或 null
30
+ */
31
+ export function getLanPeerHint(nodeHash, now = Date.now()) {
32
+ const hash = normalizeHex64(nodeHash)
33
+ if (!hash) return null
34
+ return hints.get(hash, now)
35
+ }
36
+
37
+ /**
38
+ * 清空全部 LAN peer hints(测试用)。
39
+ * @returns {void}
40
+ */
41
+ export function clearLanPeerHints() {
42
+ hints.clear()
43
+ }
@@ -38,7 +38,7 @@ export function createMdnsDiscoveryProvider(options = {}) {
38
38
  resolve()
39
39
  })
40
40
  })
41
- socket.on('message', raw => {
41
+ socket.on('message', (raw, rinfo) => {
42
42
  let packet
43
43
  try { packet = JSON.parse(String(raw)) } catch { return }
44
44
  const listeners = packet.type === 'advert'
@@ -46,8 +46,12 @@ export function createMdnsDiscoveryProvider(options = {}) {
46
46
  : signalListeners.get(String(packet.topic || ''))
47
47
  if (!listeners?.size) return
48
48
  const bytes = Uint8Array.from(Buffer.from(String(packet.data || ''), 'base64'))
49
+ const meta = {
50
+ provider: 'mdns',
51
+ address: String(rinfo?.address || ''),
52
+ }
49
53
  for (const listener of listeners)
50
- listener(bytes, { provider: 'mdns' })
54
+ listener(bytes, meta)
51
55
  })
52
56
  bound = true
53
57
  })()
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'