@steve02081504/fount-p2p 0.0.11 → 0.0.13

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 (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
@@ -18,7 +18,7 @@ export const pendingPartInvoke = new Map()
18
18
  /**
19
19
  * @typedef {{
20
20
  * send: (name: string, payload: unknown, peerId: string | null) => void
21
- * on: (name: string, handler: (payload: unknown, peerId: string) => void) => void
21
+ * on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void
22
22
  * }} PartWireAdapter
23
23
  */
24
24
 
@@ -73,35 +73,39 @@ async function dispatchPartInvoke(wireContext, payload) {
73
73
  * @param {PartWireContext} wireContext 入站上下文
74
74
  * @param {PartWireAdapter} wire Trystero 适配器
75
75
  * @param {{ allowPartInvoke?: (payload: object) => boolean }} [options] 入站过滤
76
- * @returns {void}
76
+ * @returns {() => void} 取消挂载的 dispose
77
77
  */
78
78
  export function attachPartWire(wireContext, wire, options = {}) {
79
- wire.on('part_timeline_put', data => {
80
- if (!isPlainObject(data)) return
81
- const partpath = normalizePartpath(data.partpath)
82
- if (!partpath) return
83
- const message = parsePartTimelinePut(data, partpath)
84
- if (!message) return
85
- void dispatchDeliveryInbound({
86
- replicaUsername: wireContext.replicaUsername,
87
- requesterNodeHash: data.nodeHash ? String(data.nodeHash).trim() : null,
88
- }, message)
89
- })
90
-
91
- wire.on('part_invoke', (data, peerId) => {
92
- if (!isPlainObject(data)) return
93
- if (options.allowPartInvoke?.(data) === false) return
94
- const payload = { ...data, peerId }
95
- if (payload.requestId)
96
- void handleIncomingPartInvokeRequest(wireContext, payload, wire, peerId)
97
- else
98
- void handleIncomingPartInvokeFireAndForget(wireContext, payload, wire, peerId)
99
- })
100
-
101
- wire.on('part_invoke_response', (data, peerId) => {
102
- if (!isPlainObject(data)) return
103
- handleIncomingPartInvokeResponse(data, peerId)
104
- })
79
+ const offs = [
80
+ wire.on('part_timeline_put', data => {
81
+ if (!isPlainObject(data)) return
82
+ const partpath = normalizePartpath(data.partpath)
83
+ if (!partpath) return
84
+ const message = parsePartTimelinePut(data, partpath)
85
+ if (!message) return
86
+ void dispatchDeliveryInbound({
87
+ replicaUsername: wireContext.replicaUsername,
88
+ requesterNodeHash: data.nodeHash ? String(data.nodeHash).trim() : null,
89
+ }, message)
90
+ }),
91
+ wire.on('part_invoke', (data, peerId) => {
92
+ if (!isPlainObject(data)) return
93
+ if (options.allowPartInvoke?.(data) === false) return
94
+ const payload = { ...data, peerId }
95
+ if (payload.requestId)
96
+ void handleIncomingPartInvokeRequest(wireContext, payload, wire, peerId)
97
+ else
98
+ void handleIncomingPartInvokeFireAndForget(wireContext, payload, wire, peerId)
99
+ }),
100
+ wire.on('part_invoke_response', (data, peerId) => {
101
+ if (!isPlainObject(data)) return
102
+ handleIncomingPartInvokeResponse(data, peerId)
103
+ }),
104
+ ]
105
+ return () => {
106
+ for (const off of offs)
107
+ try { off?.() } catch { /* ignore */ }
108
+ }
105
109
  }
106
110
 
107
111
  /**
@@ -116,7 +120,7 @@ export async function handleIncomingPartInvokeRequest(wireContext, payload, wire
116
120
  if (!partpath || !payload.requestId) return
117
121
 
118
122
  const response = await dispatchPartInvoke(wireContext, { ...payload, peerId })
119
- if (response == null || !isPartInvokeResponse(response)) return
123
+ if (!isPartInvokeResponse(response)) return
120
124
 
121
125
  try {
122
126
  wire.send('part_invoke_response', {
@@ -159,14 +163,12 @@ export async function handleIncomingPartInvokeFireAndForget(wireContext, payload
159
163
  * @returns {void}
160
164
  */
161
165
  export function handleIncomingPartInvokeResponse(payload, peerId = '') {
162
- const pending = pendingPartInvoke.get(String(payload?.requestId || ''))
163
- if (!pending || payload?.response == null) return
164
- const peerKey = String(peerId || payload?.nodeHash || '').trim()
165
- if (peerKey) {
166
- if (pending.respondedPeers.has(peerKey)) return
167
- pending.respondedPeers.add(peerKey)
166
+ const pending = pendingPartInvoke.get(payload.requestId)
167
+ if (!pending || !isPartInvokeResponse(payload.response)) return
168
+ if (peerId) {
169
+ if (pending.respondedPeers.has(peerId)) return
170
+ pending.respondedPeers.add(peerId)
168
171
  }
169
- if (!isPartInvokeResponse(payload.response)) return
170
172
  pending.responses.push(payload.response)
171
173
  if (pending.responses.length >= pending.maxResponses) pending.finish()
172
174
  }
@@ -45,9 +45,9 @@ export function isPartInvokeResponse(value) {
45
45
  const err = value.error
46
46
  return isPlainObject(err)
47
47
  && typeof err.message === 'string'
48
- && err.message.length > 0
49
48
  && typeof err.code === 'string'
50
- && err.code.length > 0
49
+ && err.message.length
50
+ && err.code.length
51
51
  }
52
52
  return true
53
53
  }
@@ -57,7 +57,7 @@ export function isPartInvokeResponse(value) {
57
57
  * @returns {unknown | null} 成功 `result`;失败或空为 null
58
58
  */
59
59
  export function unwrapPartInvokeResult(response) {
60
- if (!response || !isPartInvokeResponse(response) || 'error' in response) return null
60
+ if (!isPartInvokeResponse(response) || 'error' in response) return null
61
61
  return response.result ?? null
62
62
  }
63
63
 
@@ -77,5 +77,5 @@ export function normalizePartpath(value) {
77
77
  export function isPartInvoke(value) {
78
78
  if (!isPlainObject(value)) return false
79
79
  const { kind } = value
80
- return typeof kind === 'string' && kind.length > 0
80
+ return typeof kind === 'string' && kind.length
81
81
  }
@@ -9,9 +9,9 @@ import {
9
9
  parsePartQueryReq,
10
10
  parsePartQueryRes,
11
11
  } from '../schemas/part_query.mjs'
12
+ import { sendToNodeLink } from '../transport/link_registry.mjs'
12
13
  import { buildMergedGraph } from '../trust_graph/build.mjs'
13
14
  import { pickTopFromGraph } from '../trust_graph/engine.mjs'
14
- import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
15
15
  import { resolveFederationFanoutTopK } from '../trust_graph/resolve.mjs'
16
16
  import trustGraphTunables from '../trust_graph/tunables.json' with { type: 'json' }
17
17
 
@@ -132,7 +132,7 @@ export function registerQueryInboundHandler(partpath, kind, handler, state = def
132
132
  * @returns {number} 等待下游超时
133
133
  */
134
134
  export function resolvePartQueryHopTimeoutMs(ttl, tunables = partQueryTunables) {
135
- const table = Array.isArray(tunables.hopTimeoutMs) ? tunables.hopTimeoutMs : [1000, 2500, 4000, 6000]
135
+ const table = tunables.hopTimeoutMs || [1000, 2500, 4000, 6000]
136
136
  const index = Math.max(0, Math.min(table.length - 1, Math.floor(ttl) - 1))
137
137
  return Math.max(1, Math.floor(Number(table[index]) || tunables.defaultTimeoutMs || 4000))
138
138
  }
@@ -176,8 +176,7 @@ export function mergeQueryRows(lists, maxHits, rowKey) {
176
176
  async function runLocalHandler(state, queryContext, partpath, kind, query) {
177
177
  const handler = state.handlers.get(handlerKey(partpath, kind))
178
178
  if (!handler) return []
179
- const rows = await handler(queryContext, query)
180
- return Array.isArray(rows) ? rows : []
179
+ return await handler(queryContext, query) || []
181
180
  }
182
181
 
183
182
  /**
@@ -211,8 +210,9 @@ async function selectQueryNeighbors(username, exclude, dependencies) {
211
210
  * @returns {Promise<boolean>} 是否发出
212
211
  */
213
212
  async function deliverQuery(username, nodeHash, action, payload, dependencies) {
213
+ void username
214
214
  if (dependencies.deliver) return Boolean(await dependencies.deliver(nodeHash, action, payload))
215
- return requireTrustGraphProvider(DEFAULT_TRUST_GRAPH_OWNER).sendToNode(username, nodeHash, action, payload)
215
+ return sendToNodeLink(nodeHash, { scope: 'node', action, payload })
216
216
  }
217
217
 
218
218
  /**
@@ -327,27 +327,32 @@ async function processIncomingRequest(wireContext, wire, request, peerId, depend
327
327
  * @param {{ replicaUsername?: string }} wireContext 入站上下文
328
328
  * @param {PartWireAdapter} wire wire
329
329
  * @param {PartQueryDependencies} [dependencies] 可注入依赖(含 per-node state)
330
- * @returns {void}
330
+ * @returns {() => void} 取消挂载的 dispose
331
331
  */
332
332
  export function attachPartQueryWire(wireContext, wire, dependencies = {}) {
333
333
  const state = resolveState(dependencies)
334
- wire.on('part_query_req', (data, peerId) => {
335
- if (!isPlainObject(data)) return
336
- const request = parsePartQueryReq(data)
337
- if (!request) return
338
- if (!state.takeDedupe(request.requestId)) return
339
- const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
340
- if (source && !consumeWireRateBucket(`part_query:${source}`, {
341
- maxCount: partQueryTunables.ratePerSourcePerMin,
342
- })) return
343
- void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
344
- })
345
-
346
- wire.on('part_query_res', (data, peerId) => {
347
- const response = parsePartQueryRes(data)
348
- if (!response) return
349
- handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
350
- })
334
+ const offs = [
335
+ wire.on('part_query_req', (data, peerId) => {
336
+ if (!isPlainObject(data)) return
337
+ const request = parsePartQueryReq(data)
338
+ if (!request) return
339
+ if (!state.takeDedupe(request.requestId)) return
340
+ const source = String(peerId || request.originNodeHash || '').trim().toLowerCase()
341
+ if (source && !consumeWireRateBucket(`part_query:${source}`, {
342
+ maxCount: partQueryTunables.ratePerSourcePerMin,
343
+ })) return
344
+ void processIncomingRequest(wireContext, wire, request, String(peerId || ''), dependencies)
345
+ }),
346
+ wire.on('part_query_res', (data, peerId) => {
347
+ const response = parsePartQueryRes(data)
348
+ if (!response) return
349
+ handleIncomingPartQueryResponse(response, String(peerId || ''), dependencies)
350
+ }),
351
+ ]
352
+ return () => {
353
+ for (const off of offs)
354
+ try { off?.() } catch { /* ignore */ }
355
+ }
351
356
  }
352
357
 
353
358
  /**
@@ -9,6 +9,11 @@
9
9
  "dedupeMaxSize": 4000,
10
10
  "cacheTtlMs": 300000,
11
11
  "cacheMaxKeys": 256,
12
- "hopTimeoutMs": [1000, 2500, 4000, 6000],
12
+ "hopTimeoutMs": [
13
+ 1000,
14
+ 2500,
15
+ 4000,
16
+ 6000
17
+ ],
13
18
  "defaultTimeoutMs": 4000
14
19
  }
@@ -77,7 +77,7 @@ export function createPartQueryCache(options = {}) {
77
77
  */
78
78
  set(partpath, kind, query, rows, now = Date.now()) {
79
79
  const key = partQueryCacheKey(partpath, kind, query)
80
- if (!key || !Array.isArray(rows)) return
80
+ if (!key || !rows) return
81
81
  sweep(now)
82
82
  map.touch(key, {
83
83
  rows: rows.slice(0, maxHits),
@@ -17,7 +17,7 @@ export const MAX_STREAM_VOLATILE_SLICES = 256
17
17
  * @returns {object[] | null} 合法切片或 null
18
18
  */
19
19
  export function boundStreamSlices(slices) {
20
- if (!Array.isArray(slices) || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
20
+ if (!slices?.length || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
21
21
  return slices
22
22
  }
23
23
 
@@ -1,191 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
- import dgram from 'node:dgram'
3
-
4
- const DEFAULT_PORT = 53531
5
- const DEFAULT_GROUP = '239.255.42.99'
6
-
7
- /**
8
- * 轻量 multicast 发现插件:不做完整 DNS-SD,只复用 mDNS 的 LAN multicast 发现思路。
9
- * 引用计数为 0 时关闭 UDP socket,避免 shutdown 后句柄拖住进程。
10
- *
11
- * @param {{ port?: number, group?: string }} [options] 组播端口与组地址
12
- * @returns {import('./index.mjs').DiscoveryProvider} mDNS 发现提供者
13
- */
14
- export function createMdnsDiscoveryProvider(options = {}) {
15
- const port = Number(options.port) || DEFAULT_PORT
16
- const group = String(options.group || DEFAULT_GROUP)
17
- /** @type {import('node:dgram').Socket | null} */
18
- let socket = null
19
- let bound = false
20
- let bindPromise = null
21
- let refs = 0
22
- /** @type {Map<string, Set<Function>>} */
23
- const advertListeners = new Map()
24
- /** @type {Map<string, Set<Function>>} */
25
- const signalListeners = new Map()
26
-
27
- /**
28
- * 懒创建或返回当前 UDP socket。
29
- * @returns {import('node:dgram').Socket} 用于组播 bind/send 的 socket
30
- */
31
- function getSocket() {
32
- return socket ||= dgram.createSocket({ type: 'udp4', reuseAddr: true })
33
- }
34
-
35
- /**
36
- * 绑定 UDP socket 并注册组播消息处理器。
37
- * @returns {Promise<void>}
38
- */
39
- async function ensureBound() {
40
- if (bound) return
41
- if (!bindPromise)
42
- bindPromise = (async () => {
43
- const sock = getSocket()
44
- await new Promise((resolve, reject) => {
45
- sock.once('error', reject)
46
- sock.bind(port, '0.0.0.0', () => {
47
- sock.off('error', reject)
48
- sock.addMembership(group)
49
- sock.setMulticastTTL(1)
50
- resolve()
51
- })
52
- })
53
- // acquire 已全部释放:关掉刚 bind 的 socket,避免泄漏句柄。
54
- if (refs <= 0) {
55
- try { sock.close() } catch { /* ignore */ }
56
- if (socket === sock) socket = null
57
- bound = false
58
- return
59
- }
60
- sock.on('message', (raw, rinfo) => {
61
- let packet
62
- try { packet = JSON.parse(String(raw)) } catch { return }
63
- const listeners = packet.type === 'advert'
64
- ? advertListeners.get(String(packet.topic || ''))
65
- : signalListeners.get(String(packet.topic || ''))
66
- if (!listeners?.size) return
67
- const bytes = Uint8Array.from(Buffer.from(String(packet.data || ''), 'base64'))
68
- const meta = {
69
- provider: 'mdns',
70
- address: String(rinfo?.address || ''),
71
- }
72
- for (const listener of listeners)
73
- listener(bytes, meta)
74
- })
75
- bound = true
76
- })()
77
- .finally(() => {
78
- if (!bound) bindPromise = null
79
- })
80
- await bindPromise
81
- }
82
-
83
- /**
84
- * 向组播组发送 advert 或 signal 包。
85
- * @param {'advert' | 'signal'} type 包类型
86
- * @param {string} topic topic 名称
87
- * @param {Uint8Array} bytes 载荷字节
88
- * @returns {Promise<void>}
89
- */
90
- async function multicast(type, topic, bytes) {
91
- await ensureBound()
92
- const sock = getSocket()
93
- const packet = Buffer.from(JSON.stringify({
94
- type,
95
- topic,
96
- data: Buffer.from(bytes).toString('base64'),
97
- }))
98
- await new Promise((resolve, reject) => {
99
- sock.send(packet, port, group, error => error ? reject(error) : resolve())
100
- })
101
- }
102
-
103
- /**
104
- * 占用 socket;归还后若无引用则关闭。
105
- * @returns {() => void} 释放函数
106
- */
107
- function acquire() {
108
- refs++
109
- void ensureBound().catch(() => { })
110
- return () => {
111
- refs = Math.max(0, refs - 1)
112
- if (refs > 0 || !socket) return
113
- try { socket.close() } catch { /* ignore */ }
114
- socket = null
115
- bound = false
116
- bindPromise = null
117
- }
118
- }
119
-
120
- /**
121
- * 向 topic bucket 注册监听器。
122
- * @param {Map<string, Set<Function>>} bucket topic → 监听器集合
123
- * @param {string} topic 订阅 topic
124
- * @param {Function} listener 回调函数
125
- * @returns {() => void} 取消订阅函数
126
- */
127
- function addListener(bucket, topic, listener) {
128
- if (!bucket.has(topic)) bucket.set(topic, new Set())
129
- bucket.get(topic).add(listener)
130
- const release = acquire()
131
- return () => {
132
- bucket.get(topic)?.delete(listener)
133
- release()
134
- }
135
- }
136
-
137
- return {
138
- id: 'mdns',
139
- priority: 10,
140
- caps: { canDiscover: true, canSignal: true, canRelay: false },
141
- /**
142
- * 周期性组播广播 advert(首发后台,不阻塞调用方)。
143
- * @param {string} topic advert 主题
144
- * @param {Uint8Array} bytes advert 载荷
145
- * @returns {Promise<() => void>} 取消广播函数
146
- */
147
- async advertise(topic, bytes) {
148
- const release = acquire()
149
- void multicast('advert', topic, bytes).catch(() => { })
150
- const timer = setInterval(() => { void multicast('advert', topic, bytes).catch(() => { }) }, 30_000)
151
- return () => {
152
- clearInterval(timer)
153
- release()
154
- }
155
- },
156
- /**
157
- * 订阅组播 advert(立即返回;UDP bind 后台完成)。
158
- * @param {string} topic advert 主题
159
- * @param {Function} onAdvert advert 回调
160
- * @returns {Promise<() => void>} 取消订阅函数
161
- */
162
- async subscribe(topic, onAdvert) {
163
- return addListener(advertListeners, topic, onAdvert)
164
- },
165
- /**
166
- * 组播发送信令(忽略单播目标)。
167
- * @param {string} topic 信令 topic
168
- * @param {string} _to 目标标识(未使用)
169
- * @param {Uint8Array} bytes 信令载荷
170
- * @returns {Promise<void>}
171
- */
172
- async sendSignal(topic, _to, bytes) {
173
- const release = acquire()
174
- try {
175
- await multicast('signal', topic, bytes)
176
- }
177
- finally {
178
- release()
179
- }
180
- },
181
- /**
182
- * 订阅组播信令(立即返回;UDP bind 后台完成)。
183
- * @param {string} topic 信令 topic
184
- * @param {Function} onSignal 信令回调
185
- * @returns {Promise<() => void>} 取消订阅函数
186
- */
187
- async onSignal(topic, onSignal) {
188
- return addListener(signalListeners, topic, onSignal)
189
- },
190
- }
191
- }
@@ -1,82 +0,0 @@
1
- import { Buffer } from 'node:buffer'
2
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
3
-
4
- import { normalizeHex64 } from '../core/hexIds.mjs'
5
- import { sha256Hex } from '../crypto/crypto.mjs'
6
-
7
- const SIGNAL_DOMAIN = 'fount-signal'
8
- const NODE_TOPIC_DOMAIN = 'fount-rdv-node:'
9
- const GROUP_TOPIC_DOMAIN = 'fount-rdv-group:'
10
-
11
- /**
12
- * 由 nodeHash 派生节点 rendezvous topic。
13
- * @param {string} nodeHash 节点 64 hex
14
- * @returns {string} rendezvous topic 哈希
15
- */
16
- export function nodeRendezvousTopic(nodeHash) {
17
- return sha256Hex(`${NODE_TOPIC_DOMAIN}${normalizeHex64(nodeHash)}`)
18
- }
19
-
20
- /**
21
- * 由房间密钥派生群组 rendezvous topic。
22
- * @param {string} roomSecret 房间密钥
23
- * @returns {string} rendezvous topic 哈希
24
- */
25
- export function groupRendezvousTopic(roomSecret) {
26
- return sha256Hex(`${GROUP_TOPIC_DOMAIN}${String(roomSecret || '')}`)
27
- }
28
-
29
- /**
30
- * 由 topic 派生信令 AES 密钥。
31
- * @param {string} topic rendezvous 主题
32
- * @returns {Buffer} AES-256 密钥
33
- */
34
- function signalKeyForTopic(topic) {
35
- return createHash('sha256').update(`${SIGNAL_DOMAIN}:${String(topic)}`).digest()
36
- }
37
-
38
- /**
39
- * 加密信令包为 AES-GCM 字节序列。
40
- * @param {string} topic rendezvous 主题
41
- * @param {unknown} packet 待加密 JSON 对象
42
- * @returns {Uint8Array} 加密后的字节
43
- */
44
- export function encryptSignalPacket(topic, packet) {
45
- const iv = randomBytes(12)
46
- const cipher = createCipheriv('aes-256-gcm', signalKeyForTopic(topic), iv)
47
- const ciphertext = Buffer.concat([
48
- cipher.update(Buffer.from(JSON.stringify(packet), 'utf8')),
49
- cipher.final(),
50
- ])
51
- return Buffer.from(JSON.stringify({
52
- iv: iv.toString('base64'),
53
- authTag: cipher.getAuthTag().toString('base64'),
54
- ciphertext: ciphertext.toString('base64'),
55
- }))
56
- }
57
-
58
- /**
59
- * 解密信令包;失败时返回 null。
60
- * @param {string} topic rendezvous 主题
61
- * @param {Uint8Array} bytes 加密字节
62
- * @returns {object | null} 解密后的 JSON 对象
63
- */
64
- export function decryptSignalPacket(topic, bytes) {
65
- try {
66
- const payload = JSON.parse(Buffer.from(bytes).toString('utf8'))
67
- const decipher = createDecipheriv(
68
- 'aes-256-gcm',
69
- signalKeyForTopic(topic),
70
- Buffer.from(payload.iv, 'base64'),
71
- )
72
- decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'))
73
- const plain = Buffer.concat([
74
- decipher.update(Buffer.from(payload.ciphertext, 'base64')),
75
- decipher.final(),
76
- ])
77
- return JSON.parse(plain.toString('utf8'))
78
- }
79
- catch {
80
- return null
81
- }
82
- }