@livedesk/hub 0.1.53 → 0.1.55

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.53",
3
+ "version": "0.1.55",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
19
- "@livedesk/runtime-core": "0.1.6",
19
+ "@livedesk/runtime-core": "0.1.7",
20
20
  "@openai/codex-sdk": "0.145.0",
21
21
  "cors": "^2.8.5",
22
22
  "express": "^4.21.2",
@@ -465,8 +465,10 @@ export function normalizeRemoteClipboardCommandResult(action, operationId, value
465
465
  response.manifest = normalizeRemoteClipboardManifest(result.manifest, operationId);
466
466
  response.kind = response.manifest.contentKind;
467
467
  } else {
468
- const kind = result.contentKind || result.kind;
469
- if (kind) response.kind = normalizeContentKind(kind);
468
+ const explicitContentKind = String(result.contentKind || '').trim().toLowerCase();
469
+ const legacyKind = String(result.kind || '').trim().toLowerCase();
470
+ if (explicitContentKind) response.kind = normalizeContentKind(explicitContentKind);
471
+ else if (['text', 'image', 'files'].includes(legacyKind)) response.kind = legacyKind;
470
472
  }
471
473
  for (const key of ['receivedBytes', 'totalBytes']) {
472
474
  if (result[key] !== undefined) {
package/src/remote-hub.js CHANGED
@@ -1,7 +1,14 @@
1
- import net from 'net';
2
- import os from 'os';
3
- import crypto from 'crypto';
4
- import { createHubRelayControl } from './transport/relay-hub-control.js';
1
+ import net from 'net';
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+ import {
5
+ acceptSecureDirectSocket,
6
+ isDirectSecureChannel,
7
+ isLoopbackSocketAddress,
8
+ isPrivateSocketAddress,
9
+ isSecureDirectHandshakeStart
10
+ } from '../../runtime-core/src/direct-secure-transport.js';
11
+ import { createHubRelayControl } from './transport/relay-hub-control.js';
5
12
  import { parseExactLiveStreamMonitorIndex } from './live-stream-monitor-contract.js';
6
13
  import {
7
14
  BoundedSegmentedBuffer,
@@ -1334,9 +1341,10 @@ function normalizeRemoteInputEvent(value = {}) {
1334
1341
  // input must prove the exact monitor owned by its capture generation.
1335
1342
  monitorIndex,
1336
1343
  normalizedX: Number.isFinite(normalizedX) ? Math.max(0, Math.min(1, normalizedX)) : undefined,
1337
- normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
1338
- button: safeString(input.button || input.Button, 24),
1339
- deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
1344
+ normalizedY: Number.isFinite(normalizedY) ? Math.max(0, Math.min(1, normalizedY)) : undefined,
1345
+ button: safeString(input.button || input.Button, 24),
1346
+ inputSource: safeString(input.inputSource || input.InputSource, 48).toLowerCase(),
1347
+ deltaX: Number.isFinite(deltaX) ? Math.max(-4096, Math.min(4096, deltaX)) : 0,
1340
1348
  deltaY: Number.isFinite(deltaY) ? Math.max(-4096, Math.min(4096, deltaY)) : 0,
1341
1349
  key,
1342
1350
  code,
@@ -2396,9 +2404,12 @@ export function createRemoteHub(options = {}) {
2396
2404
  const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
2397
2405
  const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
2398
2406
  const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
2399
- const pairToken = safeString(
2400
- options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
2401
- 256);
2407
+ const pairToken = safeString(
2408
+ options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(32).toString('hex'),
2409
+ 256);
2410
+ const allowInsecurePrivateDirect = isEnabledValue(
2411
+ options.allowInsecureDirect ?? env.LIVEDESK_ALLOW_INSECURE_DIRECT,
2412
+ false);
2402
2413
  const relayControl = options.relayControl && typeof options.relayControl === 'object'
2403
2414
  ? options.relayControl
2404
2415
  : createHubRelayControl({
@@ -7948,10 +7959,24 @@ export function createRemoteHub(options = {}) {
7948
7959
  function handleAgentMessage(socket, state, message) {
7949
7960
  if (!message || typeof message !== 'object') {
7950
7961
  return;
7951
- }
7952
-
7953
- if (!state.authenticated) {
7954
- if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7962
+ }
7963
+
7964
+ if (!state.authenticated) {
7965
+ if (socket.__liveDeskSecureDirect === true) {
7966
+ const applicationChannel = message.type === 'slot.assign'
7967
+ ? 'control'
7968
+ : message.type === 'hello'
7969
+ ? (safeString(message.channel || message.Channel, 40).toLowerCase() || 'control')
7970
+ : '';
7971
+ if (applicationChannel
7972
+ && (!isDirectSecureChannel(applicationChannel)
7973
+ || applicationChannel !== socket.__liveDeskSecureChannel)) {
7974
+ writeJsonLine(socket, { type: 'error', error: 'direct-secure-channel-mismatch' });
7975
+ socket.end();
7976
+ return;
7977
+ }
7978
+ }
7979
+ if (message.type === 'hello' && socket.__liveDeskRelayControl === true) {
7955
7980
  message = {
7956
7981
  ...message,
7957
7982
  capabilities: {
@@ -7987,7 +8012,7 @@ export function createRemoteHub(options = {}) {
7987
8012
  return;
7988
8013
  }
7989
8014
 
7990
- const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
8015
+ const channel = safeString(message.channel || message.Channel, 40).toLowerCase() || 'control';
7991
8016
  if (channel === 'input') {
7992
8017
  const device = attachInputSocket(socket, message);
7993
8018
  if (!device) {
@@ -8537,18 +8562,60 @@ export function createRemoteHub(options = {}) {
8537
8562
  if (firstChunk) {
8538
8563
  processData(firstChunk);
8539
8564
  }
8540
- }
8541
-
8542
- function handleSocket(socket) {
8543
- socket.once('data', chunk => {
8544
- const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8545
- if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8546
- handleWebSocketUpgradeSocket(socket, firstChunk);
8547
- return;
8548
- }
8549
-
8550
- handleTcpAgentSocket(socket, firstChunk);
8551
- });
8565
+ }
8566
+
8567
+ function handleSocket(socket) {
8568
+ allSockets.add(socket);
8569
+ const prefaceTimer = setTimeout(() => {
8570
+ if (!socket.destroyed) socket.destroy();
8571
+ }, 10_000);
8572
+ prefaceTimer.unref?.();
8573
+ socket.once('close', () => {
8574
+ clearTimeout(prefaceTimer);
8575
+ allSockets.delete(socket);
8576
+ });
8577
+ socket.once('data', chunk => {
8578
+ clearTimeout(prefaceTimer);
8579
+ const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8580
+ if (isSecureDirectHandshakeStart(firstChunk)) {
8581
+ void acceptSecureDirectSocket(socket, firstChunk, pairToken)
8582
+ .then(secureSocket => {
8583
+ allSockets.delete(socket);
8584
+ handleTcpAgentSocket(secureSocket);
8585
+ })
8586
+ .catch(error => {
8587
+ if (socket.destroyed) return;
8588
+ if (error?.code === 'LIVEDESK_INVALID_PAIR_TOKEN_REJECTED') {
8589
+ const forcedCloseTimer = setTimeout(() => socket.destroy(), 1_000);
8590
+ forcedCloseTimer.unref?.();
8591
+ socket.once('close', () => clearTimeout(forcedCloseTimer));
8592
+ if (typeof socket.destroySoon === 'function') socket.destroySoon();
8593
+ else socket.end();
8594
+ return;
8595
+ }
8596
+ socket.destroy();
8597
+ });
8598
+ return;
8599
+ }
8600
+
8601
+ const remoteAddress = socket.remoteAddress;
8602
+ const allowLegacyPlaintext = isLoopbackSocketAddress(remoteAddress)
8603
+ || (allowInsecurePrivateDirect && isPrivateSocketAddress(remoteAddress));
8604
+ if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8605
+ if (!allowLegacyPlaintext) {
8606
+ socket.destroy();
8607
+ return;
8608
+ }
8609
+ handleWebSocketUpgradeSocket(socket, firstChunk);
8610
+ return;
8611
+ }
8612
+
8613
+ if (!allowLegacyPlaintext) {
8614
+ socket.destroy();
8615
+ return;
8616
+ }
8617
+ handleTcpAgentSocket(socket, firstChunk);
8618
+ });
8552
8619
 
8553
8620
  socket.once('error', () => {
8554
8621
  // The transport-specific handler owns logging after the first byte.
@@ -8570,10 +8637,10 @@ export function createRemoteHub(options = {}) {
8570
8637
  started = true;
8571
8638
  boundPort = candidateServer.address()?.port || port;
8572
8639
  lastError = '';
8573
- logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
8574
- if (host === '0.0.0.0' || host === '::') {
8575
- logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
8576
- }
8640
+ logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
8641
+ if (host === '0.0.0.0' || host === '::') {
8642
+ logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Non-loopback Direct connections require authenticated encryption.');
8643
+ }
8577
8644
  emitRemoteEvent('RemoteHubStarted', null);
8578
8645
  resolve();
8579
8646
  });