@livedesk/hub 0.1.17 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "LiveDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -42,7 +42,11 @@ export function createAgentManager({
42
42
 
43
43
  async function publicSettings() {
44
44
  const current = await settings.get();
45
- const { codexMaxTurns: _codexMaxTurns, ...exposedSettings } = current;
45
+ const {
46
+ codexMaxTurns: _codexMaxTurns,
47
+ provider: _provider,
48
+ ...exposedSettings
49
+ } = current;
46
50
  const codex = await getCodexStatus();
47
51
  const securityStatus = runtime?.getSecurityStatus?.() || {
48
52
  isolatedCodexHome: false,
@@ -55,7 +59,6 @@ export function createAgentManager({
55
59
  };
56
60
  return {
57
61
  ...exposedSettings,
58
- provider: AGENT_PROVIDER_CODEX,
59
62
  codexInstallation: codex.installed ? 'installed' : 'not-installed',
60
63
  codexAuth: codex.authenticated,
61
64
  codexStatus: codex.status,
@@ -3,10 +3,9 @@ import { access, link, mkdir, stat, unlink } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import { createRequire } from 'node:module';
7
- import { AgentProviderError } from './provider-errors.js';
8
- import { AGENT_PROVIDER_CODEX } from './agent-settings.js';
9
- import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
6
+ import { createRequire } from 'node:module';
7
+ import { AgentProviderError } from './provider-errors.js';
8
+ import { AGENT_TOOL_NAMES } from './agent-tool-registry.js';
10
9
  import { createAgentPermissionPolicy, hashAgentPermissionPolicy } from './agent-permissions.js';
11
10
 
12
11
  const require = createRequire(import.meta.url);
@@ -427,11 +426,10 @@ export function createCodexAgentRuntime({
427
426
  }
428
427
 
429
428
  function publicRun(run) {
430
- return {
431
- runId: run.runId,
432
- status: run.status,
433
- provider: AGENT_PROVIDER_CODEX,
434
- instruction: run.instruction,
429
+ return {
430
+ runId: run.runId,
431
+ status: run.status,
432
+ instruction: run.instruction,
435
433
  threadId: run.threadId,
436
434
  finalResponse: run.finalResponse,
437
435
  error: run.error,
@@ -654,10 +652,9 @@ export function createCodexAgentRuntime({
654
652
  const permissionPolicy = freezeAgentPermissionPolicy(input.permissionPolicy && typeof input.permissionPolicy === 'object'
655
653
  ? input.permissionPolicy
656
654
  : createAgentPermissionPolicy({ mode: input.permissionMode || 'safe-auto', deviceIds, maxToolCalls: settings.codexMaxToolCalls }));
657
- const run = {
658
- runId: crypto.randomUUID(),
659
- status: 'queued',
660
- provider: AGENT_PROVIDER_CODEX,
655
+ const run = {
656
+ runId: crypto.randomUUID(),
657
+ status: 'queued',
661
658
  instruction,
662
659
  deviceIds,
663
660
  threadId: '',
package/src/remote-hub.js CHANGED
@@ -4232,9 +4232,11 @@ export function createRemoteHub(options = {}) {
4232
4232
  const captureTimingAvailable = hasFrameCaptureTiming(message);
4233
4233
  const videoTransport = transport === 'udp-p2p'
4234
4234
  ? 'p2p-udp'
4235
- : transport === 'ws-binary'
4236
- ? 'direct-ws'
4237
- : 'direct-tcp';
4235
+ : transport === 'relay-binary'
4236
+ ? 'encrypted-relay'
4237
+ : transport === 'ws-binary'
4238
+ ? 'direct-ws'
4239
+ : 'direct-tcp';
4238
4240
  device.latestLiveFrame = {
4239
4241
  streamId,
4240
4242
  frameSeq,
@@ -4322,9 +4324,16 @@ export function createRemoteHub(options = {}) {
4322
4324
  deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
4323
4325
  deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
4324
4326
  videoTransport,
4325
- frameTransportActive: safeString(message.frameTransportActive, 20) || (videoTransport === 'p2p-udp' ? 'p2p' : 'direct'),
4327
+ frameTransportActive: safeString(message.frameTransportActive, 20)
4328
+ || (videoTransport === 'p2p-udp' ? 'p2p' : videoTransport === 'encrypted-relay' ? 'relay' : 'direct'),
4326
4329
  frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
4327
- || (videoTransport === 'p2p-udp' ? 'udp' : videoTransport === 'direct-ws' ? 'ws' : 'tcp'),
4330
+ || (videoTransport === 'p2p-udp'
4331
+ ? 'udp'
4332
+ : videoTransport === 'encrypted-relay'
4333
+ ? 'relay'
4334
+ : videoTransport === 'direct-ws'
4335
+ ? 'ws'
4336
+ : 'tcp'),
4328
4337
  frameTransportState: safeString(message.frameTransportState, 20) || 'active',
4329
4338
  frameTransportP2pReady: message.frameTransportP2pReady === true,
4330
4339
  frameTransportReason: safeString(message.frameTransportReason, 240),
@@ -4518,7 +4527,11 @@ export function createRemoteHub(options = {}) {
4518
4527
  }
4519
4528
 
4520
4529
  const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
4521
- const binaryTransport = state.binaryTransport === 'ws-binary' ? 'ws-binary' : 'binary';
4530
+ const binaryTransport = socket.__liveDeskRelayControl === true
4531
+ ? 'relay-binary'
4532
+ : state.binaryTransport === 'ws-binary'
4533
+ ? 'ws-binary'
4534
+ : 'binary';
4522
4535
  if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
4523
4536
  applyThumbnailFrame(device, header, framePayload, binaryTransport);
4524
4537
  return;
package/src/server.js CHANGED
@@ -2127,9 +2127,11 @@ function buildRemoteFrameBinaryPacket(frameEvent, diagnostics = {}) {
2127
2127
  transport: frame.transport || '',
2128
2128
  videoTransport: frame.videoTransport || (frame.transport === 'udp-p2p'
2129
2129
  ? 'p2p-udp'
2130
- : frame.transport === 'ws-binary'
2131
- ? 'direct-ws'
2132
- : 'direct-tcp'),
2130
+ : frame.transport === 'relay-binary'
2131
+ ? 'encrypted-relay'
2132
+ : frame.transport === 'ws-binary'
2133
+ ? 'direct-ws'
2134
+ : 'direct-tcp'),
2133
2135
  frameTransportActive: frame.frameTransportActive || '',
2134
2136
  frameTransportProtocol: frame.frameTransportProtocol || '',
2135
2137
  frameTransportState: frame.frameTransportState || '',
@@ -2,10 +2,10 @@ import crypto from 'node:crypto';
2
2
  import { lookup } from 'node:dns/promises';
3
3
  import dgram from 'node:dgram';
4
4
  import {
5
- UDP_MAX_DATAGRAM_BYTES,
6
- UDP_P2P_PROTOCOL,
7
- UdpFrameReassembler,
8
- UdpReplayWindow,
5
+ UDP_MAX_DATAGRAM_BYTES,
6
+ UDP_P2P_PROTOCOL,
7
+ UdpFrameReassembler,
8
+ UdpReplayWindow,
9
9
  decodeRendezvousMessage,
10
10
  decodeUdpPacket,
11
11
  encodeRendezvousMessage,
@@ -52,7 +52,7 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
52
52
  const socket = dgram.createSocket('udp4');
53
53
  const sessions = new Map();
54
54
  let frameHandler = () => {};
55
- let eventHandler = () => {};
55
+ let eventHandler = () => {};
56
56
  let started = false;
57
57
  let boundPort = requestedPort;
58
58
  let sequence = crypto.randomInt(1, 0x7fffffff);
@@ -65,20 +65,20 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
65
65
  if (trace) logEvent('udp', `${type} ${JSON.stringify({ deviceId: event.deviceId || '', state: event.state || '', reason: event.reason || '' })}`);
66
66
  }
67
67
 
68
- function nextSequence() {
69
- sequence = (sequence + 1) >>> 0;
70
- return sequence;
71
- }
72
-
68
+ function nextSequence() {
69
+ sequence = (sequence + 1) >>> 0;
70
+ return sequence;
71
+ }
72
+
73
73
  function sendPacket(session, type, header = {}, payload = Buffer.alloc(0), address = session.clientEndpoint || session.peerEndpoint) {
74
74
  if (!address || !session) return false;
75
- let packet;
76
- try {
77
- packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
78
- } catch (error) {
79
- logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
80
- return false;
81
- }
75
+ let packet;
76
+ try {
77
+ packet = encodeUdpPacket({ type, sessionId: session.sessionId, key: session.key, sequence: nextSequence(), header, payload });
78
+ } catch (error) {
79
+ logWarn('udp', `UDP packet rejected device=${session.deviceId}: ${error?.message || error}`);
80
+ return false;
81
+ }
82
82
  socket.send(packet, address.port, address.address);
83
83
  return true;
84
84
  }
@@ -288,9 +288,9 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
288
288
  keyBase64: key.toString('base64'),
289
289
  rendezvousToken: crypto.randomBytes(16).toString('hex'),
290
290
  sendControl,
291
- clientEndpoint: null,
292
- peerEndpoint: null,
293
- replay: new UdpReplayWindow(),
291
+ clientEndpoint: null,
292
+ peerEndpoint: null,
293
+ replay: new UdpReplayWindow(),
294
294
  reassembler: new UdpFrameReassembler({ maxPending: maxPendingFrames, timeoutMs: frameTimeoutMs }),
295
295
  ready: false,
296
296
  createdAt: Date.now(),
@@ -333,8 +333,8 @@ export function createHubUdpTransport({ env = process.env, logEvent = () => {},
333
333
  return {
334
334
  enabled,
335
335
  preferred: preferP2p,
336
- state: session.ready ? 'udp-ready' : 'udp-session-created',
337
- ready: session.ready,
336
+ state: session.ready ? 'udp-ready' : 'udp-session-created',
337
+ ready: session.ready,
338
338
  sessionId: session.sessionId,
339
339
  lastPacketAt: session.lastPacketAt ? new Date(session.lastPacketAt).toISOString() : '',
340
340
  framesReceived: session.framesReceived,
@@ -2,9 +2,11 @@ import crypto from 'node:crypto';
2
2
  import { inflateRawSync } from 'node:zlib';
3
3
 
4
4
  export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
5
- export const UDP_P2P_VERSION = 1;
6
- export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
- export const UDP_PACKET_TYPES = Object.freeze({
5
+ export const UDP_P2P_VERSION = 1;
6
+ export const UDP_MAX_DATAGRAM_BYTES = 1200;
7
+ const UDP_SERIAL_MODULUS = 0x1_0000_0000;
8
+ const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
9
+ export const UDP_PACKET_TYPES = Object.freeze({
8
10
  probe: 1,
9
11
  'probe-ack': 2,
10
12
  ready: 3,
@@ -83,13 +85,21 @@ function decodeFramedPayload(payload, legacyHeader = null) {
83
85
  }
84
86
  }
85
87
 
86
- function packetTypeCode(type) {
88
+ function packetTypeCode(type) {
87
89
  const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
88
90
  if (!code || !UDP_PACKET_TYPE_NAMES.has(code)) throw new Error('invalid-udp-packet-type');
89
91
  return code;
90
- }
91
-
92
- function encodeHeader(header) {
92
+ }
93
+
94
+ function normalizeSequence(value) {
95
+ const sequence = Number(value);
96
+ if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
97
+ throw new Error('invalid-udp-packet-sequence');
98
+ }
99
+ return sequence;
100
+ }
101
+
102
+ function encodeHeader(header) {
93
103
  const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
94
104
  if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
95
105
  return bytes;
@@ -103,9 +113,9 @@ export function createUdpSessionKey() {
103
113
  return crypto.randomBytes(32);
104
114
  }
105
115
 
106
- export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
107
- const normalizedSessionId = normalizeSessionId(sessionId);
108
- const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
116
+ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
117
+ const normalizedSessionId = normalizeSessionId(sessionId);
118
+ const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
109
119
  const headerBytes = encodeHeader(header);
110
120
  const payloadBytes = asBuffer(payload);
111
121
  if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
@@ -117,8 +127,8 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
117
127
  preamble[6] = sessionBytes.length;
118
128
  preamble.writeUInt16BE(headerBytes.length, 7);
119
129
  preamble.writeUInt16BE(payloadBytes.length, 9);
120
- preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
121
- const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
130
+ preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
131
+ const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
122
132
  const associatedData = Buffer.concat([preamble, nonce, sessionBytes, headerBytes]);
123
133
  const cipher = crypto.createCipheriv('aes-256-gcm', normalizeKey(key), nonce);
124
134
  cipher.setAAD(associatedData);
@@ -169,26 +179,41 @@ export function decodeUdpPacket(packet, key) {
169
179
  }
170
180
  }
171
181
 
172
- export class UdpReplayWindow {
182
+ export class UdpReplayWindow {
173
183
  constructor(size = 512) {
174
184
  this.size = Math.max(32, Math.min(4096, Math.floor(Number(size) || 512)));
175
185
  this.highest = -1;
176
186
  this.seen = new Set();
177
187
  }
178
188
 
179
- accept(sequence) {
180
- const value = Number(sequence) >>> 0;
181
- if (this.highest >= 0 && value + this.size < this.highest) return false;
182
- if (this.seen.has(value)) return false;
183
- this.seen.add(value);
184
- if (this.highest < 0 || value > this.highest) this.highest = value;
185
- const minimum = Math.max(0, this.highest - this.size);
186
- for (const item of this.seen) {
187
- if (item < minimum) this.seen.delete(item);
188
- }
189
- return true;
190
- }
191
- }
189
+ accept(sequence) {
190
+ let value;
191
+ try {
192
+ value = normalizeSequence(sequence);
193
+ } catch {
194
+ return false;
195
+ }
196
+ if (this.seen.has(value)) return false;
197
+ if (this.highest >= 0) {
198
+ const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
199
+ if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
200
+ if (forward > UDP_SERIAL_HALF_RANGE) {
201
+ const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
202
+ if (age > this.size) return false;
203
+ } else {
204
+ this.highest = value;
205
+ }
206
+ } else {
207
+ this.highest = value;
208
+ }
209
+ this.seen.add(value);
210
+ for (const item of this.seen) {
211
+ const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
212
+ if (age > this.size) this.seen.delete(item);
213
+ }
214
+ return true;
215
+ }
216
+ }
192
217
 
193
218
  export class UdpFrameReassembler {
194
219
  constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {