@livedesk/hub 0.1.35 → 0.1.37

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.
@@ -1,21 +1,21 @@
1
- import crypto from 'node:crypto';
2
- import { lookup } from 'node:dns/promises';
3
- import dgram from 'node:dgram';
1
+ import crypto from 'node:crypto';
2
+ import { lookup } from 'node:dns/promises';
3
+ import dgram from 'node:dgram';
4
4
  import {
5
- UDP_MAX_DATAGRAM_BYTES,
6
- UDP_FRAME_CHUNK_PROTOCOL,
7
- UDP_P2P_PROTOCOL,
8
- UdpFrameReassembler,
9
- UdpReplayWindow,
10
- decodeRendezvousMessage,
11
- decodeUdpPacket,
5
+ UDP_MAX_DATAGRAM_BYTES,
6
+ UDP_FRAME_CHUNK_PROTOCOL,
7
+ UDP_P2P_PROTOCOL,
8
+ UdpFrameReassembler,
9
+ UdpReplayWindow,
10
+ decodeRendezvousMessage,
11
+ decodeUdpPacket,
12
12
  encodeRendezvousMessage,
13
13
  encodeUdpPacket,
14
- createUdpSessionKey,
15
- isUdpBinaryDatagram,
16
- peekUdpSessionRouteKey,
17
- udpSessionRouteKey
18
- } from './udp-protocol.js';
14
+ createUdpSessionKey,
15
+ isUdpBinaryDatagram,
16
+ peekUdpSessionRouteKey,
17
+ udpSessionRouteKey
18
+ } from './udp-protocol.js';
19
19
 
20
20
  function isEnabled(value, fallback = false) {
21
21
  if (value === undefined || value === null || value === '') return fallback;
@@ -31,207 +31,206 @@ function numberEnv(value, min, max, fallback) {
31
31
  return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.floor(number))) : fallback;
32
32
  }
33
33
 
34
- function sameEndpoint(left, right) {
35
- return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
36
- }
37
-
38
- function normalizedAddress(value) {
39
- const address = safeText(value, 128).toLowerCase();
40
- return address.startsWith('::ffff:') ? address.slice(7) : address;
41
- }
42
-
43
- function isPublicIPv4(value) {
44
- const address = normalizedAddress(value);
45
- const octets = address.split('.').map(Number);
46
- if (octets.length !== 4
47
- || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
48
- return false;
49
- }
50
- const [a, b] = octets;
51
- const c = octets[2];
52
- return a !== 0
53
- && a !== 10
54
- && a !== 127
55
- && !(a === 100 && b >= 64 && b <= 127)
56
- && !(a === 169 && b === 254)
57
- && !(a === 172 && b >= 16 && b <= 31)
58
- && !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
59
- && !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
60
- && !(a === 203 && b === 0 && c === 113)
61
- && !(a >= 224);
62
- }
63
-
64
- export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
65
- const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
66
- const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
34
+ function sameEndpoint(left, right) {
35
+ return !!left && !!right && String(left.address) === String(right.address) && Number(left.port) === Number(right.port);
36
+ }
37
+
38
+ function normalizedAddress(value) {
39
+ const address = safeText(value, 128).toLowerCase();
40
+ return address.startsWith('::ffff:') ? address.slice(7) : address;
41
+ }
42
+
43
+ function isPublicIPv4(value) {
44
+ const address = normalizedAddress(value);
45
+ const octets = address.split('.').map(Number);
46
+ if (octets.length !== 4
47
+ || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
48
+ return false;
49
+ }
50
+ const [a, b] = octets;
51
+ const c = octets[2];
52
+ return a !== 0
53
+ && a !== 10
54
+ && a !== 127
55
+ && !(a === 100 && b >= 64 && b <= 127)
56
+ && !(a === 169 && b === 254)
57
+ && !(a === 172 && b >= 16 && b <= 31)
58
+ && !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
59
+ && !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
60
+ && !(a === 203 && b === 0 && c === 113)
61
+ && !(a >= 224);
62
+ }
63
+
64
+ export function createHubUdpTransport({ env = process.env, logEvent = () => {}, logWarn = () => {} } = {}) {
65
+ const enabled = isEnabled(env.LIVEDESK_UDP_ENABLED, true);
66
+ const preferP2p = isEnabled(env.LIVEDESK_UDP_PREFER_P2P, false);
67
67
  const bindHost = safeText(env.LIVEDESK_UDP_BIND_HOST || '0.0.0.0', 128) || '0.0.0.0';
68
68
  const requestedPort = numberEnv(env.LIVEDESK_UDP_PORT, 0, 65535, 0);
69
69
  const advertiseHost = safeText(env.LIVEDESK_UDP_ADVERTISE_HOST || env.LIVEDESK_REMOTE_PUBLIC_HOST || '', 128);
70
- const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
71
- const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
72
- const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
70
+ const rendezvousHost = safeText(env.LIVEDESK_UDP_RENDEZVOUS_HOST || 'www.gnsoftech.com', 128);
71
+ const rendezvousPort = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_PORT, 1, 65535, 5199);
72
+ const rendezvousRefreshMs = numberEnv(env.LIVEDESK_UDP_RENDEZVOUS_REFRESH_MS, 1000, 50_000, 25_000);
73
73
  const punchTimeoutMs = numberEnv(env.LIVEDESK_UDP_PUNCH_TIMEOUT_MS, 500, 10_000, 3000);
74
- const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
74
+ const frameTimeoutMs = numberEnv(env.LIVEDESK_UDP_FRAME_TIMEOUT_MS, 20, 2000, 500);
75
75
  const maxPendingFrames = numberEnv(env.LIVEDESK_UDP_MAX_PENDING_FRAMES, 1, 16, 2);
76
- const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
77
- const socket = dgram.createSocket('udp4');
78
- const sessions = new Map();
79
- const sessionsBySessionId = new Map();
80
- const sessionsByRouteKey = new Map();
81
- const resource = {
82
- datagramsReceived: 0,
83
- binaryDatagramsReceived: 0,
84
- rendezvousJsonParseAttempts: 0,
85
- binaryJsonParseAttempts: 0,
86
- sessionLookupCount: 0,
87
- linearSessionScanCount: 0,
88
- authenticatedDatagrams: 0,
89
- rejectedDatagrams: 0
90
- };
76
+ const trace = isEnabled(env.LIVEDESK_UDP_TRACE, false);
77
+ const socket = dgram.createSocket('udp4');
78
+ const sessions = new Map();
79
+ const sessionsBySessionId = new Map();
80
+ const sessionsByRouteKey = new Map();
81
+ const resource = {
82
+ datagramsReceived: 0,
83
+ binaryDatagramsReceived: 0,
84
+ rendezvousJsonParseAttempts: 0,
85
+ binaryJsonParseAttempts: 0,
86
+ sessionLookupCount: 0,
87
+ linearSessionScanCount: 0,
88
+ authenticatedDatagrams: 0,
89
+ rejectedDatagrams: 0
90
+ };
91
91
  let frameHandler = () => {};
92
- let eventHandler = () => {};
93
- let started = false;
94
- let boundPort = requestedPort;
95
- let sequence = crypto.randomInt(1, 0x7fffffff);
96
- let rendezvousAddresses = new Set();
97
- let rendezvousRefreshTimer = null;
98
- let rendezvousRefreshPromise = null;
99
- let frameExpiryTimer = null;
100
- let rendezvousProofIssuer = null;
101
- const peerProbeBurstDelaysMs = [0, 120, 360];
92
+ let eventHandler = () => {};
93
+ let started = false;
94
+ let boundPort = requestedPort;
95
+ let sequence = crypto.randomInt(1, 0x7fffffff);
96
+ let rendezvousAddresses = new Set();
97
+ let rendezvousRefreshTimer = null;
98
+ let rendezvousRefreshPromise = null;
99
+ let frameExpiryTimer = null;
100
+ const peerProbeBurstDelaysMs = [0, 120, 360];
102
101
 
103
102
  function emit(type, event = {}) {
104
103
  eventHandler(type, event);
105
104
  if (trace) logEvent('udp', `${type} ${JSON.stringify({ deviceId: event.deviceId || '', state: event.state || '', reason: event.reason || '' })}`);
106
105
  }
107
106
 
108
- function nextSequence() {
109
- sequence = (sequence + 1) >>> 0;
110
- return sequence;
111
- }
112
-
113
- function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
114
- if (!address || !session) return false;
115
- let packet;
116
- try {
117
- packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
118
- } catch (error) {
119
- logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
120
- return false;
121
- }
107
+ function nextSequence() {
108
+ sequence = (sequence + 1) >>> 0;
109
+ return sequence;
110
+ }
111
+
112
+ function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
113
+ if (!address || !session) return false;
114
+ let packet;
115
+ try {
116
+ packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
117
+ } catch (error) {
118
+ logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
119
+ return false;
120
+ }
122
121
  socket.send(packet, address.port, address.address);
123
122
  return true;
124
123
  }
125
124
 
126
- function sendRendezvousRegister(session) {
127
- if (!rendezvousHost || rendezvousAddresses.size === 0 || !started) return false;
128
- const message = encodeRendezvousMessage({
129
- type: 'rendezvous.register',
125
+ function sendRendezvousRegister(session) {
126
+ if (!rendezvousHost || rendezvousAddresses.size === 0 || !started) return false;
127
+ const message = encodeRendezvousMessage({
128
+ type: 'rendezvous.register',
130
129
  protocol: UDP_P2P_PROTOCOL,
131
130
  roomId: session.sessionId,
132
- role: 'hub',
133
- token: session.rendezvousToken
134
- });
135
- let sent = false;
136
- for (const address of rendezvousAddresses) {
137
- try {
138
- socket.send(message, rendezvousPort, address, error => {
139
- if (error) logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
140
- });
141
- sent = true;
142
- } catch (error) {
143
- logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
144
- }
145
- }
146
- return sent;
147
- }
148
-
149
- async function resolveRendezvousAddresses() {
150
- if (!rendezvousHost) return false;
151
- try {
152
- const records = await lookup(rendezvousHost, { all: true, family: 4, verbatim: true });
153
- const resolved = new Set(records.map(record => normalizedAddress(record.address)).filter(Boolean));
154
- if (resolved.size === 0) throw new Error('no-ipv4-address');
155
- rendezvousAddresses = resolved;
156
- return true;
157
- } catch (error) {
158
- logWarn('udp', `UDP rendezvous host resolution failed: ${error?.message || error}`);
159
- return false;
160
- }
161
- }
162
-
163
- function refreshRendezvousRegistrations(reason = 'periodic') {
164
- if (!started || !rendezvousHost) return Promise.resolve(false);
165
- if (rendezvousRefreshPromise) return rendezvousRefreshPromise;
166
- rendezvousRefreshPromise = (async () => {
167
- await resolveRendezvousAddresses();
168
- if (!started) return false;
169
- let refreshed = false;
170
- for (const session of sessions.values()) {
171
- refreshed = sendRendezvousRegister(session) || refreshed;
172
- }
173
- if (trace && refreshed) emit('UdpRendezvousRegistrationRefreshed', { reason });
174
- return refreshed;
175
- })().finally(() => {
176
- rendezvousRefreshPromise = null;
177
- });
178
- return rendezvousRefreshPromise;
179
- }
180
-
181
- function startRendezvousRefreshTimer() {
182
- if (!rendezvousHost || rendezvousRefreshTimer) return;
183
- rendezvousRefreshTimer = setInterval(() => {
184
- void refreshRendezvousRegistrations('periodic');
185
- }, rendezvousRefreshMs);
186
- rendezvousRefreshTimer.unref?.();
187
- }
188
-
189
- function stopRendezvousRefreshTimer() {
190
- if (rendezvousRefreshTimer) clearInterval(rendezvousRefreshTimer);
191
- rendezvousRefreshTimer = null;
192
- }
193
-
194
- function startFrameExpiryTimer() {
195
- if (frameExpiryTimer) return;
196
- frameExpiryTimer = setInterval(() => {
197
- const now = Date.now();
198
- for (const session of sessions.values()) {
199
- session.reassembler.expire(now);
200
- }
201
- }, Math.max(20, Math.min(250, frameTimeoutMs)));
202
- frameExpiryTimer.unref?.();
203
- }
204
-
205
- function stopFrameExpiryTimer() {
206
- if (frameExpiryTimer) clearInterval(frameExpiryTimer);
207
- frameExpiryTimer = null;
208
- }
209
-
210
- function isTrustedRendezvousSource(rinfo) {
211
- return Number(rinfo?.port) === rendezvousPort
212
- && rendezvousAddresses.has(normalizedAddress(rinfo?.address));
213
- }
214
-
215
- function sessionForPacket(packet) {
216
- resource.sessionLookupCount += 1;
217
- const routeKey = peekUdpSessionRouteKey(packet);
218
- return routeKey === null ? null : sessionsByRouteKey.get(routeKey) || null;
219
- }
220
-
221
- function bindAuthenticatedClientEndpoint(session, rinfo) {
222
- const endpoint = { address: rinfo.address, port: rinfo.port };
223
- if (sameEndpoint(session.clientEndpoint, endpoint)) return false;
224
- session.clientEndpoint = endpoint;
225
- session.lastPeerAt = Date.now();
226
- sendRendezvousRegister(session);
227
- void refreshRendezvousRegistrations('authenticated-endpoint-change');
228
- emit('UdpAuthenticatedEndpointObserved', {
229
- deviceId: session.deviceId,
230
- endpoint: `${endpoint.address}:${endpoint.port}`
231
- });
232
- return true;
233
- }
234
-
131
+ role: 'hub',
132
+ token: session.rendezvousToken
133
+ });
134
+ let sent = false;
135
+ for (const address of rendezvousAddresses) {
136
+ try {
137
+ socket.send(message, rendezvousPort, address, error => {
138
+ if (error) logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
139
+ });
140
+ sent = true;
141
+ } catch (error) {
142
+ logWarn('udp', `UDP rendezvous registration failed: ${error?.message || error}`);
143
+ }
144
+ }
145
+ return sent;
146
+ }
147
+
148
+ async function resolveRendezvousAddresses() {
149
+ if (!rendezvousHost) return false;
150
+ try {
151
+ const records = await lookup(rendezvousHost, { all: true, family: 4, verbatim: true });
152
+ const resolved = new Set(records.map(record => normalizedAddress(record.address)).filter(Boolean));
153
+ if (resolved.size === 0) throw new Error('no-ipv4-address');
154
+ rendezvousAddresses = resolved;
155
+ return true;
156
+ } catch (error) {
157
+ logWarn('udp', `UDP rendezvous host resolution failed: ${error?.message || error}`);
158
+ return false;
159
+ }
160
+ }
161
+
162
+ function refreshRendezvousRegistrations(reason = 'periodic') {
163
+ if (!started || !rendezvousHost) return Promise.resolve(false);
164
+ if (rendezvousRefreshPromise) return rendezvousRefreshPromise;
165
+ rendezvousRefreshPromise = (async () => {
166
+ await resolveRendezvousAddresses();
167
+ if (!started) return false;
168
+ let refreshed = false;
169
+ for (const session of sessions.values()) {
170
+ refreshed = sendRendezvousRegister(session) || refreshed;
171
+ }
172
+ if (trace && refreshed) emit('UdpRendezvousRegistrationRefreshed', { reason });
173
+ return refreshed;
174
+ })().finally(() => {
175
+ rendezvousRefreshPromise = null;
176
+ });
177
+ return rendezvousRefreshPromise;
178
+ }
179
+
180
+ function startRendezvousRefreshTimer() {
181
+ if (!rendezvousHost || rendezvousRefreshTimer) return;
182
+ rendezvousRefreshTimer = setInterval(() => {
183
+ void refreshRendezvousRegistrations('periodic');
184
+ }, rendezvousRefreshMs);
185
+ rendezvousRefreshTimer.unref?.();
186
+ }
187
+
188
+ function stopRendezvousRefreshTimer() {
189
+ if (rendezvousRefreshTimer) clearInterval(rendezvousRefreshTimer);
190
+ rendezvousRefreshTimer = null;
191
+ }
192
+
193
+ function startFrameExpiryTimer() {
194
+ if (frameExpiryTimer) return;
195
+ frameExpiryTimer = setInterval(() => {
196
+ const now = Date.now();
197
+ for (const session of sessions.values()) {
198
+ session.reassembler.expire(now);
199
+ }
200
+ }, Math.max(20, Math.min(250, frameTimeoutMs)));
201
+ frameExpiryTimer.unref?.();
202
+ }
203
+
204
+ function stopFrameExpiryTimer() {
205
+ if (frameExpiryTimer) clearInterval(frameExpiryTimer);
206
+ frameExpiryTimer = null;
207
+ }
208
+
209
+ function isTrustedRendezvousSource(rinfo) {
210
+ return Number(rinfo?.port) === rendezvousPort
211
+ && rendezvousAddresses.has(normalizedAddress(rinfo?.address));
212
+ }
213
+
214
+ function sessionForPacket(packet) {
215
+ resource.sessionLookupCount += 1;
216
+ const routeKey = peekUdpSessionRouteKey(packet);
217
+ return routeKey === null ? null : sessionsByRouteKey.get(routeKey) || null;
218
+ }
219
+
220
+ function bindAuthenticatedClientEndpoint(session, rinfo) {
221
+ const endpoint = { address: rinfo.address, port: rinfo.port };
222
+ if (sameEndpoint(session.clientEndpoint, endpoint)) return false;
223
+ session.clientEndpoint = endpoint;
224
+ session.lastPeerAt = Date.now();
225
+ sendRendezvousRegister(session);
226
+ void refreshRendezvousRegistrations('authenticated-endpoint-change');
227
+ emit('UdpAuthenticatedEndpointObserved', {
228
+ deviceId: session.deviceId,
229
+ endpoint: `${endpoint.address}:${endpoint.port}`
230
+ });
231
+ return true;
232
+ }
233
+
235
234
  function clearPeerProbeTimers(session) {
236
235
  if (!session?.peerProbeTimers) return;
237
236
  for (const timer of session.peerProbeTimers) clearTimeout(timer);
@@ -242,7 +241,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
242
241
  clearPeerProbeTimers(session);
243
242
  for (const delayMs of peerProbeBurstDelaysMs) {
244
243
  const sendProbe = () => {
245
- if (sessions.get(session.deviceId) !== session
244
+ if (sessions.get(session.deviceId) !== session
246
245
  || !sameEndpoint(session.peerEndpoint, endpoint)) return;
247
246
  sendPacket(session, 'probe', { punch: true }, Buffer.alloc(0), endpoint);
248
247
  };
@@ -259,78 +258,78 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
259
258
  }
260
259
  }
261
260
 
262
- function onRendezvousPeer(message, rinfo) {
263
- const session = sessionsBySessionId.get(safeText(message.roomId, 160));
264
- if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
265
- const address = safeText(message.host, 128);
266
- const port = Number(message.port);
267
- if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
268
- const observedAddress = normalizedAddress(message.observedHost);
269
- const observedPort = Number(message.observedPort);
270
- if (observedAddress
271
- && Number.isInteger(observedPort)
272
- && observedPort >= 1
273
- && observedPort <= 65535) {
274
- session.hubRendezvousEndpoint = {
275
- address: observedAddress,
276
- port: observedPort
277
- };
278
- session.hubRendezvousObservedAt = Date.now();
279
- }
261
+ function onRendezvousPeer(message, rinfo) {
262
+ const session = sessionsBySessionId.get(safeText(message.roomId, 160));
263
+ if (!session || message.protocol !== UDP_P2P_PROTOCOL || !isTrustedRendezvousSource(rinfo)) return;
264
+ const address = safeText(message.host, 128);
265
+ const port = Number(message.port);
266
+ if (!address || !Number.isInteger(port) || port < 1 || port > 65535) return;
267
+ const observedAddress = normalizedAddress(message.observedHost);
268
+ const observedPort = Number(message.observedPort);
269
+ if (observedAddress
270
+ && Number.isInteger(observedPort)
271
+ && observedPort >= 1
272
+ && observedPort <= 65535) {
273
+ session.hubRendezvousEndpoint = {
274
+ address: observedAddress,
275
+ port: observedPort
276
+ };
277
+ session.hubRendezvousObservedAt = Date.now();
278
+ }
280
279
  session.peerEndpoint = { address, port };
281
280
  session.lastPeerAt = Date.now();
282
281
  schedulePeerProbeBurst(session, session.peerEndpoint);
283
282
  emit('UdpPeerEndpointObserved', { deviceId: session.deviceId, endpoint: `${address}:${port}` });
284
283
  }
285
284
 
286
- socket.on('message', (payload, rinfo) => {
287
- resource.datagramsReceived += 1;
288
- if (!isUdpBinaryDatagram(payload)) {
289
- // Rendezvous messages are the only JSON datagrams. Binary packets
290
- // never pay a speculative JSON parse/exception before routing.
291
- if (payload?.[0] !== 0x7b) {
292
- resource.rejectedDatagrams += 1;
293
- return;
294
- }
295
- resource.rendezvousJsonParseAttempts += 1;
296
- const rendezvous = decodeRendezvousMessage(payload);
297
- if (rendezvous?.type === 'rendezvous.peer') {
298
- onRendezvousPeer(rendezvous, rinfo);
299
- } else {
300
- resource.rejectedDatagrams += 1;
301
- }
302
- return;
303
- }
304
- resource.binaryDatagramsReceived += 1;
305
- const session = sessionForPacket(payload);
306
- if (!session) {
307
- resource.rejectedDatagrams += 1;
308
- return;
309
- }
310
- session.resource.binaryDatagramsReceived += 1;
311
- session.resource.decryptAttempts += 1;
312
- const packet = decodeUdpPacket(payload, session.key, {
313
- id: session.sessionId,
314
- bytes: session.sessionIdBytes
315
- });
316
- if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) {
317
- session.resource.rejectedDatagrams += 1;
318
- resource.rejectedDatagrams += 1;
319
- return;
320
- }
321
- resource.authenticatedDatagrams += 1;
322
- session.resource.authenticatedDatagrams += 1;
323
- bindAuthenticatedClientEndpoint(session, rinfo);
324
- session.lastPacketAt = Date.now();
325
- if (packet.type === 'probe') {
326
- sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
285
+ socket.on('message', (payload, rinfo) => {
286
+ resource.datagramsReceived += 1;
287
+ if (!isUdpBinaryDatagram(payload)) {
288
+ // Rendezvous messages are the only JSON datagrams. Binary packets
289
+ // never pay a speculative JSON parse/exception before routing.
290
+ if (payload?.[0] !== 0x7b) {
291
+ resource.rejectedDatagrams += 1;
292
+ return;
293
+ }
294
+ resource.rendezvousJsonParseAttempts += 1;
295
+ const rendezvous = decodeRendezvousMessage(payload);
296
+ if (rendezvous?.type === 'rendezvous.peer') {
297
+ onRendezvousPeer(rendezvous, rinfo);
298
+ } else {
299
+ resource.rejectedDatagrams += 1;
300
+ }
301
+ return;
302
+ }
303
+ resource.binaryDatagramsReceived += 1;
304
+ const session = sessionForPacket(payload);
305
+ if (!session) {
306
+ resource.rejectedDatagrams += 1;
307
+ return;
308
+ }
309
+ session.resource.binaryDatagramsReceived += 1;
310
+ session.resource.decryptAttempts += 1;
311
+ const packet = decodeUdpPacket(payload, session.key, {
312
+ id: session.sessionId,
313
+ bytes: session.sessionIdBytes
314
+ });
315
+ if (!packet || packet.sessionId !== session.sessionId || !session.replay.accept(packet.sequence)) {
316
+ session.resource.rejectedDatagrams += 1;
317
+ resource.rejectedDatagrams += 1;
318
+ return;
319
+ }
320
+ resource.authenticatedDatagrams += 1;
321
+ session.resource.authenticatedDatagrams += 1;
322
+ bindAuthenticatedClientEndpoint(session, rinfo);
323
+ session.lastPacketAt = Date.now();
324
+ if (packet.type === 'probe') {
325
+ sendPacket(session, 'probe-ack', { observedHost: rinfo.address, observedPort: rinfo.port });
327
326
  emit('UdpProbeReceived', { deviceId: session.deviceId });
328
327
  return;
329
328
  }
330
329
  if (packet.type === 'ready') {
331
330
  clearPeerProbeTimers(session);
332
- session.ready = true;
333
- sendPacket(session, 'pong', { state: 'ready' });
331
+ session.ready = true;
332
+ sendPacket(session, 'pong', { state: 'ready' });
334
333
  session.sendControl?.({
335
334
  type: 'udp.session.ready',
336
335
  protocol: UDP_P2P_PROTOCOL,
@@ -347,53 +346,53 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
347
346
  return;
348
347
  }
349
348
  if (packet.type !== 'frame') return;
350
- const complete = session.reassembler.ingest(packet);
351
- if (!complete) return;
352
- session.framesReceived += 1;
353
- const reassembly = session.reassembler.getStats();
354
- const handled = frameHandler({
355
- deviceId: session.deviceId,
356
- sessionId: session.sessionId,
357
- header: {
358
- ...complete.header,
359
- hubIngressDropped: Math.max(
360
- Number(complete.header?.hubIngressDropped || 0) || 0,
361
- reassembly.droppedFrames
362
- ),
363
- udpIncompleteFrames: reassembly.droppedFrames
364
- },
365
- payload: complete.payload,
366
- transport: 'udp-p2p'
367
- });
368
- const accepted = handled === true || handled?.accepted === true;
369
- const proofId = safeText(complete.header?.frameTransportProofId, 96);
370
- if (proofId) {
371
- if (handled?.suppressProofResponse === true) return;
372
- const proofAccepted = accepted && handled?.proofAccepted === true;
373
- sendPacket(session, 'pong', {
374
- state: proofAccepted ? 'frame-proof-ack' : 'frame-proof-reject',
375
- proofId,
376
- protocol: 'livedesk.frame-transport-proof.v1',
377
- streamId: safeText(complete.header?.streamId, 128),
378
- commandId: safeText(complete.header?.commandId, 128),
379
- streamPurpose: safeText(complete.header?.streamPurpose, 24),
380
- captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
381
- monitorIndex: Number(complete.header?.monitorIndex ?? -1),
382
- error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
383
- });
384
- return;
385
- }
386
- if (accepted) {
387
- sendPacket(session, 'pong', {
388
- state: 'frame-ack',
389
- frameSeq: Number(complete.header?.frameSeq || 0) || 0,
390
- streamId: safeText(complete.header?.streamId, 128),
391
- commandId: safeText(complete.header?.commandId, 128),
392
- streamPurpose: safeText(complete.header?.streamPurpose, 24),
393
- captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
394
- monitorIndex: Number(complete.header?.monitorIndex ?? -1)
395
- });
396
- }
349
+ const complete = session.reassembler.ingest(packet);
350
+ if (!complete) return;
351
+ session.framesReceived += 1;
352
+ const reassembly = session.reassembler.getStats();
353
+ const handled = frameHandler({
354
+ deviceId: session.deviceId,
355
+ sessionId: session.sessionId,
356
+ header: {
357
+ ...complete.header,
358
+ hubIngressDropped: Math.max(
359
+ Number(complete.header?.hubIngressDropped || 0) || 0,
360
+ reassembly.droppedFrames
361
+ ),
362
+ udpIncompleteFrames: reassembly.droppedFrames
363
+ },
364
+ payload: complete.payload,
365
+ transport: 'udp-p2p'
366
+ });
367
+ const accepted = handled === true || handled?.accepted === true;
368
+ const proofId = safeText(complete.header?.frameTransportProofId, 96);
369
+ if (proofId) {
370
+ if (handled?.suppressProofResponse === true) return;
371
+ const proofAccepted = accepted && handled?.proofAccepted === true;
372
+ sendPacket(session, 'pong', {
373
+ state: proofAccepted ? 'frame-proof-ack' : 'frame-proof-reject',
374
+ proofId,
375
+ protocol: 'livedesk.frame-transport-proof.v1',
376
+ streamId: safeText(complete.header?.streamId, 128),
377
+ commandId: safeText(complete.header?.commandId, 128),
378
+ streamPurpose: safeText(complete.header?.streamPurpose, 24),
379
+ captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
380
+ monitorIndex: Number(complete.header?.monitorIndex ?? -1),
381
+ error: proofAccepted ? '' : safeText(handled?.error || 'frame-proof-rejected', 160)
382
+ });
383
+ return;
384
+ }
385
+ if (accepted) {
386
+ sendPacket(session, 'pong', {
387
+ state: 'frame-ack',
388
+ frameSeq: Number(complete.header?.frameSeq || 0) || 0,
389
+ streamId: safeText(complete.header?.streamId, 128),
390
+ commandId: safeText(complete.header?.commandId, 128),
391
+ streamPurpose: safeText(complete.header?.streamPurpose, 24),
392
+ captureGeneration: Number(complete.header?.captureGeneration || 0) || 0,
393
+ monitorIndex: Number(complete.header?.monitorIndex ?? -1)
394
+ });
395
+ }
397
396
  });
398
397
  socket.on('error', error => logWarn('udp', `UDP socket error: ${error?.message || error}`));
399
398
 
@@ -405,93 +404,74 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
405
404
  socket.once('error', onError);
406
405
  socket.once('listening', onListening);
407
406
  socket.bind(requestedPort, bindHost);
408
- });
409
- started = true;
410
- boundPort = socket.address().port;
411
- try {
412
- socket.setRecvBufferSize(8 * 1024 * 1024);
413
- socket.setSendBufferSize(1024 * 1024);
414
- } catch (error) {
415
- logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
416
- }
417
- logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
418
- void refreshRendezvousRegistrations('startup');
419
- startRendezvousRefreshTimer();
420
- startFrameExpiryTimer();
421
- return getStatus();
407
+ });
408
+ started = true;
409
+ boundPort = socket.address().port;
410
+ try {
411
+ socket.setRecvBufferSize(8 * 1024 * 1024);
412
+ socket.setSendBufferSize(1024 * 1024);
413
+ } catch (error) {
414
+ logWarn('udp', `UDP socket buffer tuning was unavailable: ${error?.message || error}`);
415
+ }
416
+ logEvent('udp', `UDP P2P socket listening on udp://${bindHost}:${boundPort}`);
417
+ void refreshRendezvousRegistrations('startup');
418
+ startRendezvousRefreshTimer();
419
+ startFrameExpiryTimer();
420
+ return getStatus();
422
421
  }
423
422
 
424
- function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
425
- if (!enabled || capabilities?.udpP2p !== true || !started) return null;
426
- const id = safeText(deviceId, 160);
427
- if (!id) return null;
428
- const previous = sessions.get(id);
423
+ function registerDevice({ deviceId, sessionId: tcpSessionId, sendControl, capabilities } = {}) {
424
+ if (!enabled || capabilities?.udpP2p !== true || !started) return null;
425
+ const id = safeText(deviceId, 160);
426
+ if (!id) return null;
427
+ const previous = sessions.get(id);
429
428
  previous?.sendControl?.({ type: 'udp.session.close', reason: 'replaced' });
430
429
  if (previous) {
431
- clearPeerProbeTimers(previous);
432
- sessions.delete(id);
433
- sessionsBySessionId.delete(previous.sessionId);
434
- sessionsByRouteKey.delete(previous.routeKey);
435
- previous.reassembler.clear();
436
- }
437
- const key = createUdpSessionKey();
438
- let udpSessionId;
439
- let routeKey;
440
- do {
441
- udpSessionId = crypto.randomUUID();
442
- routeKey = udpSessionRouteKey(udpSessionId);
443
- } while (sessionsByRouteKey.has(routeKey));
444
- const hubProof = typeof rendezvousProofIssuer === 'function'
445
- ? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'hub', ttlMs: 60_000 })
446
- : null;
447
- const clientProof = typeof rendezvousProofIssuer === 'function'
448
- ? rendezvousProofIssuer({ roomId: udpSessionId, deviceId: id, role: 'client', ttlMs: 60_000 })
449
- : null;
450
- const signedHubRendezvousProof = safeText(hubProof?.proof, 2048);
451
- const signedClientRendezvousProof = safeText(clientProof?.proof, 2048);
452
- const allowLegacyUnsignedProofForTests = env.LIVEDESK_TEST_MODE === '1'
453
- || env.LIVEDESK_TEST_ALLOW_LEGACY_UDP_RENDEZVOUS === '1';
454
- const legacyRendezvousToken = allowLegacyUnsignedProofForTests
455
- && (!signedHubRendezvousProof || !signedClientRendezvousProof)
456
- ? crypto.randomBytes(16).toString('hex')
457
- : '';
458
- const proofExpiries = [hubProof?.payload?.expiresAt, clientProof?.payload?.expiresAt]
459
- .map(Number)
460
- .filter(Number.isSafeInteger);
461
- const session = {
430
+ clearPeerProbeTimers(previous);
431
+ sessions.delete(id);
432
+ sessionsBySessionId.delete(previous.sessionId);
433
+ sessionsByRouteKey.delete(previous.routeKey);
434
+ previous.reassembler.clear();
435
+ }
436
+ const key = createUdpSessionKey();
437
+ let udpSessionId;
438
+ let routeKey;
439
+ do {
440
+ udpSessionId = crypto.randomUUID();
441
+ routeKey = udpSessionRouteKey(udpSessionId);
442
+ } while (sessionsByRouteKey.has(routeKey));
443
+ const session = {
462
444
  deviceId: id,
463
445
  tcpSessionId: safeText(tcpSessionId, 160),
464
- sessionId: udpSessionId,
465
- sessionIdBytes: Buffer.from(udpSessionId, 'utf8'),
466
- routeKey,
446
+ sessionId: udpSessionId,
447
+ sessionIdBytes: Buffer.from(udpSessionId, 'utf8'),
448
+ routeKey,
467
449
  key,
468
450
  keyBase64: key.toString('base64'),
469
- rendezvousToken: signedHubRendezvousProof || legacyRendezvousToken,
470
- clientRendezvousToken: signedClientRendezvousProof || legacyRendezvousToken,
471
- rendezvousProofExpiresAt: proofExpiries.length === 2 ? Math.min(...proofExpiries) : 0,
451
+ rendezvousToken: crypto.randomBytes(16).toString('hex'),
472
452
  sendControl,
473
- clientEndpoint: null,
474
- peerEndpoint: null,
475
- hubRendezvousEndpoint: null,
476
- hubRendezvousObservedAt: 0,
477
- replay: new UdpReplayWindow(),
478
- reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
479
- resource: {
480
- binaryDatagramsReceived: 0,
481
- decryptAttempts: 0,
482
- authenticatedDatagrams: 0,
483
- rejectedDatagrams: 0
484
- },
453
+ clientEndpoint: null,
454
+ peerEndpoint: null,
455
+ hubRendezvousEndpoint: null,
456
+ hubRendezvousObservedAt: 0,
457
+ replay: new UdpReplayWindow(),
458
+ reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
459
+ resource: {
460
+ binaryDatagramsReceived: 0,
461
+ decryptAttempts: 0,
462
+ authenticatedDatagrams: 0,
463
+ rejectedDatagrams: 0
464
+ },
485
465
  ready: false,
486
466
  peerProbeTimers: new Set(),
487
467
  createdAt: Date.now(),
488
468
  lastPacketAt: 0,
489
469
  framesReceived: 0
490
- };
491
- sessions.set(id, session);
492
- sessionsBySessionId.set(session.sessionId, session);
493
- sessionsByRouteKey.set(session.routeKey, session);
494
- sendControl?.({
470
+ };
471
+ sessions.set(id, session);
472
+ sessionsBySessionId.set(session.sessionId, session);
473
+ sessionsByRouteKey.set(session.routeKey, session);
474
+ sendControl?.({
495
475
  type: 'udp.session.offer',
496
476
  protocol: UDP_P2P_PROTOCOL,
497
477
  sessionId: session.sessionId,
@@ -500,206 +480,203 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
500
480
  port: boundPort,
501
481
  expiresInMs: punchTimeoutMs + 15_000,
502
482
  punchTimeoutMs,
503
- maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
504
- frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
505
- frameTimeoutMs,
483
+ maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
484
+ frameChunkHeader: UDP_FRAME_CHUNK_PROTOCOL,
485
+ frameTimeoutMs,
506
486
  maxPendingFrames,
507
- rendezvous: rendezvousHost && session.clientRendezvousToken
508
- ? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.clientRendezvousToken }
509
- : null
487
+ rendezvous: rendezvousHost
488
+ ? { host: rendezvousHost, port: rendezvousPort, roomId: session.sessionId, token: session.rendezvousToken }
489
+ : null
510
490
  });
511
491
  sendRendezvousRegister(session);
512
492
  emit('UdpSessionCreated', { deviceId: id, state: 'udp-session-created' });
513
493
  return { sessionId: session.sessionId, ready: false };
514
494
  }
515
495
 
516
- function unregisterDevice(deviceId, tcpSessionId = '') {
496
+ function unregisterDevice(deviceId, tcpSessionId = '') {
517
497
  const id = safeText(deviceId, 160);
518
498
  const session = sessions.get(id);
519
- if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
499
+ if (!session || (tcpSessionId && session.tcpSessionId !== tcpSessionId)) return;
520
500
  clearPeerProbeTimers(session);
521
501
  sessions.delete(id);
522
- sessionsBySessionId.delete(session.sessionId);
523
- sessionsByRouteKey.delete(session.routeKey);
524
- session.reassembler.clear();
525
- emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
502
+ sessionsBySessionId.delete(session.sessionId);
503
+ sessionsByRouteKey.delete(session.routeKey);
504
+ session.reassembler.clear();
505
+ emit('UdpSessionClosed', { deviceId: id, reason: 'tcp-session-closed' });
526
506
  }
527
507
 
528
- function getDeviceStatus(deviceId) {
508
+ function getDeviceStatus(deviceId) {
529
509
  const session = sessions.get(safeText(deviceId, 160));
530
510
  if (!session) return { enabled, state: enabled ? 'disabled' : 'tcp-only', ready: false };
531
511
  return {
532
512
  enabled,
533
513
  preferred: preferP2p,
534
- state: session.ready ? 'udp-ready' : 'udp-session-created',
535
- ready: session.ready,
514
+ state: session.ready ? 'udp-ready' : 'udp-session-created',
515
+ ready: session.ready,
536
516
  sessionId: session.sessionId,
537
- lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
538
- framesReceived: session.framesReceived,
539
- resource: {
540
- ...session.resource,
541
- binaryJsonParseAttempts: 0,
542
- sessionLookup: 'map-o1',
543
- linearSessionScanCount: 0
544
- },
545
- ...session.reassembler.getStats()
546
- };
547
- }
548
-
549
- async function refreshDeviceDiagnosticNetwork(deviceId, { timeoutMs = 3_000 } = {}) {
550
- const session = sessions.get(safeText(deviceId, 160));
551
- if (!session || !started || !rendezvousHost) return false;
552
- const requestedAt = Date.now();
553
- await resolveRendezvousAddresses();
554
- if (!sendRendezvousRegister(session)) return false;
555
- const deadline = requestedAt + numberEnv(timeoutMs, 250, 5_000, 3_000);
556
- while (Date.now() < deadline) {
557
- if (session.hubRendezvousObservedAt >= requestedAt) return true;
558
- await new Promise(resolve => {
559
- const timer = setTimeout(resolve, 40);
560
- timer.unref?.();
561
- });
562
- }
563
- return false;
564
- }
565
-
566
- function getDeviceDiagnosticNetwork(deviceId, { salt = '' } = {}) {
567
- const session = sessions.get(safeText(deviceId, 160));
568
- const clientAddress = normalizedAddress(session?.clientEndpoint?.address);
569
- const normalizedHubAddress = normalizedAddress(session?.hubRendezvousEndpoint?.address);
570
- const hubAddressObservedAt = session?.hubRendezvousObservedAt
571
- ? new Date(session.hubRendezvousObservedAt).toISOString()
572
- : '';
573
- const digest = value => value
574
- ? crypto.createHash('sha256')
575
- .update(`${safeText(salt, 160)}:${value}`)
576
- .digest('hex')
577
- .slice(0, 16)
578
- : '';
579
- if (!session?.ready || !clientAddress) {
580
- return {
581
- evidence: 'authenticated-udp-endpoint-unavailable',
582
- differentPublicEgress: null,
583
- clientAddressHash: '',
584
- hubAddressHash: digest(normalizedHubAddress),
585
- hubAddressObservedAt,
586
- hubEndpointSource: normalizedHubAddress
587
- ? 'trusted-rendezvous-observation'
588
- : 'unavailable'
589
- };
590
- }
591
- const clientAddressPublic = isPublicIPv4(clientAddress);
592
- const hubAddressPublic = isPublicIPv4(normalizedHubAddress);
593
- return {
594
- evidence: normalizedHubAddress
595
- ? 'authenticated-udp-endpoint-rendezvous-observed'
596
- : 'authenticated-udp-endpoint-hub-egress-unavailable',
597
- differentPublicEgress: clientAddressPublic && hubAddressPublic
598
- ? clientAddress !== normalizedHubAddress
599
- : null,
600
- clientAddressHash: digest(clientAddress),
601
- hubAddressHash: digest(normalizedHubAddress),
602
- clientAddressPublic,
603
- hubAddressPublic,
604
- endpointAuthenticated: true,
605
- udpReady: session.ready === true,
606
- observedAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
607
- hubAddressObservedAt,
608
- hubEndpointSource: normalizedHubAddress
609
- ? 'trusted-rendezvous-observation'
610
- : 'unavailable'
611
- };
612
- }
613
-
614
- async function close() {
615
- stopRendezvousRefreshTimer();
616
- stopFrameExpiryTimer();
517
+ lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
518
+ framesReceived: session.framesReceived,
519
+ resource: {
520
+ ...session.resource,
521
+ binaryJsonParseAttempts: 0,
522
+ sessionLookup: 'map-o1',
523
+ linearSessionScanCount: 0
524
+ },
525
+ ...session.reassembler.getStats()
526
+ };
527
+ }
528
+
529
+ async function refreshDeviceDiagnosticNetwork(deviceId, { timeoutMs = 3_000 } = {}) {
530
+ const session = sessions.get(safeText(deviceId, 160));
531
+ if (!session || !started || !rendezvousHost) return false;
532
+ const requestedAt = Date.now();
533
+ await resolveRendezvousAddresses();
534
+ if (!sendRendezvousRegister(session)) return false;
535
+ const deadline = requestedAt + numberEnv(timeoutMs, 250, 5_000, 3_000);
536
+ while (Date.now() < deadline) {
537
+ if (session.hubRendezvousObservedAt >= requestedAt) return true;
538
+ await new Promise(resolve => {
539
+ const timer = setTimeout(resolve, 40);
540
+ timer.unref?.();
541
+ });
542
+ }
543
+ return false;
544
+ }
545
+
546
+ function getDeviceDiagnosticNetwork(deviceId, { salt = '' } = {}) {
547
+ const session = sessions.get(safeText(deviceId, 160));
548
+ const clientAddress = normalizedAddress(session?.clientEndpoint?.address);
549
+ const normalizedHubAddress = normalizedAddress(session?.hubRendezvousEndpoint?.address);
550
+ const hubAddressObservedAt = session?.hubRendezvousObservedAt
551
+ ? new Date(session.hubRendezvousObservedAt).toISOString()
552
+ : '';
553
+ const digest = value => value
554
+ ? crypto.createHash('sha256')
555
+ .update(`${safeText(salt, 160)}:${value}`)
556
+ .digest('hex')
557
+ .slice(0, 16)
558
+ : '';
559
+ if (!session?.ready || !clientAddress) {
560
+ return {
561
+ evidence: 'authenticated-udp-endpoint-unavailable',
562
+ differentPublicEgress: null,
563
+ clientAddressHash: '',
564
+ hubAddressHash: digest(normalizedHubAddress),
565
+ hubAddressObservedAt,
566
+ hubEndpointSource: normalizedHubAddress
567
+ ? 'trusted-rendezvous-observation'
568
+ : 'unavailable'
569
+ };
570
+ }
571
+ const clientAddressPublic = isPublicIPv4(clientAddress);
572
+ const hubAddressPublic = isPublicIPv4(normalizedHubAddress);
573
+ return {
574
+ evidence: normalizedHubAddress
575
+ ? 'authenticated-udp-endpoint-rendezvous-observed'
576
+ : 'authenticated-udp-endpoint-hub-egress-unavailable',
577
+ differentPublicEgress: clientAddressPublic && hubAddressPublic
578
+ ? clientAddress !== normalizedHubAddress
579
+ : null,
580
+ clientAddressHash: digest(clientAddress),
581
+ hubAddressHash: digest(normalizedHubAddress),
582
+ clientAddressPublic,
583
+ hubAddressPublic,
584
+ endpointAuthenticated: true,
585
+ udpReady: session.ready === true,
586
+ observedAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
587
+ hubAddressObservedAt,
588
+ hubEndpointSource: normalizedHubAddress
589
+ ? 'trusted-rendezvous-observation'
590
+ : 'unavailable'
591
+ };
592
+ }
593
+
594
+ async function close() {
595
+ stopRendezvousRefreshTimer();
596
+ stopFrameExpiryTimer();
617
597
  for (const session of sessions.values()) {
618
598
  clearPeerProbeTimers(session);
619
599
  session.reassembler.clear();
620
- }
621
- sessions.clear();
622
- sessionsBySessionId.clear();
623
- sessionsByRouteKey.clear();
624
- if (!started) return;
625
- started = false;
626
- await new Promise(resolve => socket.close(() => resolve()));
600
+ }
601
+ sessions.clear();
602
+ sessionsBySessionId.clear();
603
+ sessionsByRouteKey.clear();
604
+ if (!started) return;
605
+ started = false;
606
+ await new Promise(resolve => socket.close(() => resolve()));
627
607
  }
628
608
 
629
609
  return {
630
610
  start,
631
611
  close,
632
612
  registerDevice,
633
- unregisterDevice,
634
- getDeviceStatus,
635
- refreshDeviceDiagnosticNetwork,
636
- getDeviceDiagnosticNetwork,
637
- getStatus,
638
- setRendezvousProofIssuer: issuer => {
639
- rendezvousProofIssuer = typeof issuer === 'function' ? issuer : null;
640
- },
613
+ unregisterDevice,
614
+ getDeviceStatus,
615
+ refreshDeviceDiagnosticNetwork,
616
+ getDeviceDiagnosticNetwork,
617
+ getStatus,
641
618
  setFrameHandler: handler => { frameHandler = typeof handler === 'function' ? handler : () => {}; },
642
619
  setEventHandler: handler => { eventHandler = typeof handler === 'function' ? handler : () => {}; }
643
620
  };
644
621
 
645
- function getStatus() {
646
- let readySessionCount = 0;
647
- const reassembly = {
648
- pendingFrames: 0,
649
- pendingBytes: 0,
650
- pendingChunkBuffers: 0,
651
- pendingBytesHighWatermark: 0,
652
- pendingChunkBuffersHighWatermark: 0,
653
- droppedFrames: 0,
654
- completedFrames: 0,
655
- completedPayloadCopyCount: 0,
656
- completedPayloadCopyBytes: 0,
657
- fullWireDuplicateCopyCount: 0,
658
- binaryChunkHeaderCount: 0,
659
- legacyChunkHeaderCount: 0
660
- };
661
- for (const session of sessions.values()) {
662
- if (session.ready) readySessionCount += 1;
663
- const stats = session.reassembler.getStats();
664
- reassembly.pendingFrames += stats.pendingFrames;
665
- reassembly.pendingBytes += stats.pendingBytes;
666
- reassembly.pendingChunkBuffers += stats.pendingChunkBuffers;
667
- reassembly.pendingBytesHighWatermark = Math.max(
668
- reassembly.pendingBytesHighWatermark,
669
- stats.pendingBytesHighWatermark
670
- );
671
- reassembly.pendingChunkBuffersHighWatermark = Math.max(
672
- reassembly.pendingChunkBuffersHighWatermark,
673
- stats.pendingChunkBuffersHighWatermark
674
- );
675
- reassembly.droppedFrames += stats.droppedFrames;
676
- reassembly.completedFrames += stats.completedFrames;
677
- reassembly.completedPayloadCopyCount += stats.completedPayloadCopyCount;
678
- reassembly.completedPayloadCopyBytes += stats.completedPayloadCopyBytes;
679
- reassembly.fullWireDuplicateCopyCount += stats.fullWireDuplicateCopyCount;
680
- reassembly.binaryChunkHeaderCount += stats.binaryChunkHeaderCount;
681
- reassembly.legacyChunkHeaderCount += stats.legacyChunkHeaderCount;
682
- }
683
- return {
622
+ function getStatus() {
623
+ let readySessionCount = 0;
624
+ const reassembly = {
625
+ pendingFrames: 0,
626
+ pendingBytes: 0,
627
+ pendingChunkBuffers: 0,
628
+ pendingBytesHighWatermark: 0,
629
+ pendingChunkBuffersHighWatermark: 0,
630
+ droppedFrames: 0,
631
+ completedFrames: 0,
632
+ completedPayloadCopyCount: 0,
633
+ completedPayloadCopyBytes: 0,
634
+ fullWireDuplicateCopyCount: 0,
635
+ binaryChunkHeaderCount: 0,
636
+ legacyChunkHeaderCount: 0
637
+ };
638
+ for (const session of sessions.values()) {
639
+ if (session.ready) readySessionCount += 1;
640
+ const stats = session.reassembler.getStats();
641
+ reassembly.pendingFrames += stats.pendingFrames;
642
+ reassembly.pendingBytes += stats.pendingBytes;
643
+ reassembly.pendingChunkBuffers += stats.pendingChunkBuffers;
644
+ reassembly.pendingBytesHighWatermark = Math.max(
645
+ reassembly.pendingBytesHighWatermark,
646
+ stats.pendingBytesHighWatermark
647
+ );
648
+ reassembly.pendingChunkBuffersHighWatermark = Math.max(
649
+ reassembly.pendingChunkBuffersHighWatermark,
650
+ stats.pendingChunkBuffersHighWatermark
651
+ );
652
+ reassembly.droppedFrames += stats.droppedFrames;
653
+ reassembly.completedFrames += stats.completedFrames;
654
+ reassembly.completedPayloadCopyCount += stats.completedPayloadCopyCount;
655
+ reassembly.completedPayloadCopyBytes += stats.completedPayloadCopyBytes;
656
+ reassembly.fullWireDuplicateCopyCount += stats.fullWireDuplicateCopyCount;
657
+ reassembly.binaryChunkHeaderCount += stats.binaryChunkHeaderCount;
658
+ reassembly.legacyChunkHeaderCount += stats.legacyChunkHeaderCount;
659
+ }
660
+ return {
684
661
  enabled,
685
662
  preferred: preferP2p,
686
663
  started,
687
664
  bindHost,
688
665
  port: boundPort,
689
666
  advertiseHost,
690
- rendezvousHost,
691
- rendezvousPort,
692
- rendezvousRefreshMs,
693
- rendezvousResolvedAddressCount: rendezvousAddresses.size,
694
- maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
695
- sessionCount: sessions.size,
696
- readySessionCount,
697
- resource: {
698
- ...resource,
699
- indexedSessionCount: sessionsBySessionId.size,
700
- indexedRouteCount: sessionsByRouteKey.size,
701
- ...reassembly
702
- }
703
- };
667
+ rendezvousHost,
668
+ rendezvousPort,
669
+ rendezvousRefreshMs,
670
+ rendezvousResolvedAddressCount: rendezvousAddresses.size,
671
+ maxDatagramBytes: UDP_MAX_DATAGRAM_BYTES,
672
+ sessionCount: sessions.size,
673
+ readySessionCount,
674
+ resource: {
675
+ ...resource,
676
+ indexedSessionCount: sessionsBySessionId.size,
677
+ indexedRouteCount: sessionsByRouteKey.size,
678
+ ...reassembly
679
+ }
680
+ };
704
681
  }
705
682
  }