@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.
- package/README.md +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
package/wire/part_ingress.mjs
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
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(
|
|
163
|
-
if (!pending || payload
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
}
|
package/wire/part_invoke.mjs
CHANGED
|
@@ -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.
|
|
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 (!
|
|
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
|
|
80
|
+
return typeof kind === 'string' && kind.length
|
|
81
81
|
}
|
package/wire/part_query.mjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
/**
|
|
@@ -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 || !
|
|
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 (!
|
|
20
|
+
if (!slices?.length || slices.length > MAX_STREAM_VOLATILE_SLICES) return null
|
|
21
21
|
return slices
|
|
22
22
|
}
|
|
23
23
|
|
package/discovery/mdns.mjs
DELETED
|
@@ -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
|
-
}
|