@livedesk/hub 0.1.41 → 0.1.43

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/src/server.js CHANGED
@@ -7,8 +7,9 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, wri
7
7
  import { dirname, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import os from 'node:os';
10
- import { WebSocketServer } from 'ws';
11
- import { createRemoteHub } from './remote-hub.js';
10
+ import { WebSocketServer } from 'ws';
11
+ import { createRemoteHub } from './remote-hub.js';
12
+ import { createHubConsoleRelay } from './console-relay.js';
12
13
  import {
13
14
  buildImmutableRemoteFramePacket,
14
15
  createRemoteFramePacketMetrics,
@@ -51,13 +52,13 @@ import { normalizeRuntimeAuthSession, runtimeRoleError } from '../../runtime-cor
51
52
  import { fetchAuthResponse, AUTH_REQUEST_TIMEOUT_MS } from '../../runtime-core/src/auth-http.js';
52
53
  import { PRODUCTION_SUPABASE_PUBLISHABLE_KEY, PRODUCTION_SUPABASE_URL } from '../../runtime-core/src/auth-config.js';
53
54
  import { createHubRuntime } from './runtime/hub-runtime.js';
54
- import { hubRuntimeStatus } from './runtime/hub-runtime-status.js';
55
- import { roleGuardResponse } from './http/hub-role-guard.js';
56
- import {
57
- clearHubUiSessionCookie,
58
- createHubUiSessionAuthority,
59
- serializeHubUiSessionCookie
60
- } from './http/hub-ui-session.js';
55
+ import { hubRuntimeStatus } from './runtime/hub-runtime-status.js';
56
+ import { roleGuardResponse } from './http/hub-role-guard.js';
57
+ import {
58
+ clearHubUiSessionCookie,
59
+ createHubUiSessionAuthority,
60
+ serializeHubUiSessionCookie
61
+ } from './http/hub-ui-session.js';
61
62
 
62
63
  const __dirname = dirname(fileURLToPath(import.meta.url));
63
64
  const webDistCandidates = [
@@ -187,10 +188,10 @@ const persistentSessionGcToken = persistentSessionGcEnabled
187
188
  ? String(process.env.LIVEDESK_PERSISTENT_SESSION_TEST_TOKEN || '').trim()
188
189
  : '';
189
190
  let connectedDeviceCount = 0;
190
- let runtimeAccessToken = '';
191
- let runtimeRefreshToken = '';
192
- let runtimeAccessTokenExpiresAt = 0;
193
- const hubUiSessionAuthority = createHubUiSessionAuthority();
191
+ let runtimeAccessToken = '';
192
+ let runtimeRefreshToken = '';
193
+ let runtimeAccessTokenExpiresAt = 0;
194
+ const hubUiSessionAuthority = createHubUiSessionAuthority();
194
195
  let roleWatchInFlight = false;
195
196
  let verifiedLicense = {
196
197
  userId: '',
@@ -202,15 +203,22 @@ let verifiedLicense = {
202
203
  let frameClientSeq = 0;
203
204
  let inputClientSeq = 0;
204
205
  let audioClientSeq = 0;
205
- let liveDeskUpdateManager = null;
206
- let hubTransferJobs = null;
206
+ let liveDeskUpdateManager = null;
207
+ let hubTransferJobs = null;
208
+ let hubConsoleRelay = null;
207
209
  const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
208
210
  const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
209
211
  readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
210
212
  Math.max(1000, HUB_HOST_TARGET_LEASE_MS - 1000)
211
213
  ));
212
- const HUB_ACCESS_TOKEN_REFRESH_SKEW_MS = 90_000;
213
- const hubWakeBaseUrl = String(process.env.LIVEDESK_WAKE_URL || 'https://livedesk-wake.lovecrdm.workers.dev').trim();
214
+ const HUB_ACCESS_TOKEN_REFRESH_SKEW_MS = 90_000;
215
+ const hubWakeBaseUrl = String(process.env.LIVEDESK_WAKE_URL || 'https://livedesk-wake.lovecrdm.workers.dev').trim();
216
+ const hubConsoleRelayBaseUrl = String(
217
+ process.env.LIVEDESK_CONSOLE_RELAY_URL
218
+ || (process.env.LIVEDESK_TEST_MODE === '1' || process.env.LIVEDESK_AUTH_TEST_MODE === '1'
219
+ ? 'off'
220
+ : 'https://livedesk-wake.lovecrdm.workers.dev')
221
+ ).trim();
214
222
  const HUB_WAKE_NOTIFY_RETRY_MS = 60_000;
215
223
  let hubHostTargetRenewTimer = null;
216
224
  let hubHostTargetRenewInFlight = false;
@@ -491,12 +499,12 @@ function clearPersistedRuntimeSession() {
491
499
  return writePrivateRuntimeAuthState(state);
492
500
  }
493
501
 
494
- function clearRuntimeSession() {
495
- runtimeAccessToken = '';
496
- runtimeRefreshToken = '';
497
- runtimeAccessTokenExpiresAt = 0;
498
- hubUiSessionAuthority.revokeAll();
499
- runtimeManager.setAuthenticated(false);
502
+ function clearRuntimeSession() {
503
+ runtimeAccessToken = '';
504
+ runtimeRefreshToken = '';
505
+ runtimeAccessTokenExpiresAt = 0;
506
+ hubUiSessionAuthority.revokeAll();
507
+ runtimeManager.setAuthenticated(false);
500
508
  try { clearPersistedRuntimeSession(); } catch { /* runtime auth is already invalid */ }
501
509
  }
502
510
 
@@ -796,11 +804,12 @@ function getLiveDeskUpdateStatus() {
796
804
  }
797
805
 
798
806
  if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
799
- liveDeskUpdateManager = createLiveDeskUpdateManager({
800
- remoteHub,
801
- currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
802
- currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
803
- restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
807
+ liveDeskUpdateManager = createLiveDeskUpdateManager({
808
+ remoteHub,
809
+ currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
810
+ currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
811
+ excludedDeviceIds: [runtimeDeviceId],
812
+ restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
804
813
  requestHubRestart
805
814
  });
806
815
  }
@@ -1332,9 +1341,16 @@ const hubSharedFolders = createHubSharedFolders({
1332
1341
  dataDir: agentDataDir
1333
1342
  });
1334
1343
 
1335
- const app = express();
1336
- const httpServer = createServer(app);
1337
- const httpConnections = new Set();
1344
+ const app = express();
1345
+ const httpServer = createServer(app);
1346
+ hubConsoleRelay = createHubConsoleRelay({
1347
+ url: runtimeRole === 'hub' ? hubConsoleRelayBaseUrl : 'off',
1348
+ deviceId: runtimeDeviceId,
1349
+ httpBaseUrl: `http://127.0.0.1:${httpPort}`,
1350
+ getAccessToken: () => getRuntimeAccessToken(),
1351
+ logger: console
1352
+ });
1353
+ const httpConnections = new Set();
1338
1354
  const frameWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1339
1355
  const atlasWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
1340
1356
  const inputWss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
@@ -1412,7 +1428,7 @@ function requestHostname(req) {
1412
1428
  }
1413
1429
  }
1414
1430
 
1415
- function isTrustedBrowserRequest(req) {
1431
+ function isTrustedBrowserRequest(req) {
1416
1432
  const origin = String(req.headers.origin || '').trim();
1417
1433
  if (!origin) {
1418
1434
  const fetchSite = String(req.headers['sec-fetch-site'] || '').toLowerCase();
@@ -1429,59 +1445,59 @@ function isTrustedBrowserRequest(req) {
1429
1445
  const targetHost = requestHostname(req);
1430
1446
  return (originHost === targetHost && isPrivateHubHostname(targetHost))
1431
1447
  || (isLoopbackHostname(originHost) && isLoopbackHostname(targetHost));
1432
- } catch {
1433
- return false;
1434
- }
1435
- }
1436
-
1437
- function isSecureHubHttpRequest(req) {
1438
- return req.socket?.encrypted === true
1439
- || String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https';
1440
- }
1441
-
1442
- function currentBoundHubUserId() {
1443
- return String(
1444
- runtimeManager.getSnapshot().userId
1445
- || readPersistedRuntimeSession()?.user?.id
1446
- || ''
1447
- ).trim();
1448
- }
1449
-
1450
- function isPublicHubApiRequest(req) {
1451
- const method = String(req.method || 'GET').toUpperCase();
1452
- const path = String(req.path || '');
1453
- return (method === 'GET' && (path === '/api/health' || path === '/api/runtime/status' || path === '/api/auth/status'))
1454
- || (method === 'POST' && path === '/api/auth/session');
1455
- }
1456
-
1457
- function isTrustedNativeLoopbackRequest(req) {
1458
- return isLoopbackAddress(req.socket?.remoteAddress)
1459
- && !String(req.headers.origin || '').trim()
1460
- && !String(req.headers['sec-fetch-site'] || '').trim();
1461
- }
1462
-
1463
- function authorizeHubUiRequest(req) {
1464
- if (process.env.LIVEDESK_TEST_MODE === '1') {
1465
- return { ok: true, explicitTestMode: true };
1466
- }
1467
- if (isTrustedNativeLoopbackRequest(req)) {
1468
- return { ok: true, nativeLoopback: true };
1469
- }
1470
- return hubUiSessionAuthority.authorize(req, currentBoundHubUserId());
1471
- }
1472
-
1473
- app.use((req, res, next) => {
1448
+ } catch {
1449
+ return false;
1450
+ }
1451
+ }
1452
+
1453
+ function isSecureHubHttpRequest(req) {
1454
+ return req.socket?.encrypted === true
1455
+ || String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https';
1456
+ }
1457
+
1458
+ function currentBoundHubUserId() {
1459
+ return String(
1460
+ runtimeManager.getSnapshot().userId
1461
+ || readPersistedRuntimeSession()?.user?.id
1462
+ || ''
1463
+ ).trim();
1464
+ }
1465
+
1466
+ function isPublicHubApiRequest(req) {
1467
+ const method = String(req.method || 'GET').toUpperCase();
1468
+ const path = String(req.path || '');
1469
+ return (method === 'GET' && (path === '/api/health' || path === '/api/runtime/status' || path === '/api/auth/status'))
1470
+ || (method === 'POST' && path === '/api/auth/session');
1471
+ }
1472
+
1473
+ function isTrustedNativeLoopbackRequest(req) {
1474
+ return isLoopbackAddress(req.socket?.remoteAddress)
1475
+ && !String(req.headers.origin || '').trim()
1476
+ && !String(req.headers['sec-fetch-site'] || '').trim();
1477
+ }
1478
+
1479
+ function authorizeHubUiRequest(req) {
1480
+ if (process.env.LIVEDESK_TEST_MODE === '1') {
1481
+ return { ok: true, explicitTestMode: true };
1482
+ }
1483
+ if (isTrustedNativeLoopbackRequest(req)) {
1484
+ return { ok: true, nativeLoopback: true };
1485
+ }
1486
+ return hubUiSessionAuthority.authorize(req, currentBoundHubUserId());
1487
+ }
1488
+
1489
+ app.use((req, res, next) => {
1474
1490
  if (!isTrustedBrowserRequest(req)) {
1475
1491
  res.status(403).json({ ok: false, error: 'untrusted-hub-origin' });
1476
1492
  return;
1477
1493
  }
1478
1494
  const origin = String(req.headers.origin || '').trim();
1479
1495
  if (origin) {
1480
- res.setHeader('Access-Control-Allow-Origin', origin);
1481
- res.setHeader('Access-Control-Allow-Credentials', 'true');
1482
- res.setHeader('Access-Control-Allow-Private-Network', 'true');
1496
+ res.setHeader('Access-Control-Allow-Origin', origin);
1497
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
1498
+ res.setHeader('Access-Control-Allow-Private-Network', 'true');
1483
1499
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
1484
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-LiveDesk-CSRF');
1500
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-LiveDesk-CSRF');
1485
1501
  res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
1486
1502
  }
1487
1503
  if (req.method === 'OPTIONS') {
@@ -1490,21 +1506,21 @@ app.use((req, res, next) => {
1490
1506
  }
1491
1507
  next();
1492
1508
  });
1493
- app.use(express.json({ limit: '32mb' }));
1494
- app.use((req, res, next) => {
1495
- if (!req.path.startsWith('/api/') || isPublicHubApiRequest(req)) {
1496
- next();
1497
- return;
1498
- }
1499
- const authorization = authorizeHubUiRequest(req);
1500
- if (!authorization.ok) {
1501
- noStore(res);
1502
- res.status(authorization.status).json({ ok: false, error: authorization.error });
1503
- return;
1504
- }
1505
- next();
1506
- });
1507
- app.use((req, res, next) => {
1509
+ app.use(express.json({ limit: '32mb' }));
1510
+ app.use((req, res, next) => {
1511
+ if (!req.path.startsWith('/api/') || isPublicHubApiRequest(req)) {
1512
+ next();
1513
+ return;
1514
+ }
1515
+ const authorization = authorizeHubUiRequest(req);
1516
+ if (!authorization.ok) {
1517
+ noStore(res);
1518
+ res.status(authorization.status).json({ ok: false, error: authorization.error });
1519
+ return;
1520
+ }
1521
+ next();
1522
+ });
1523
+ app.use((req, res, next) => {
1508
1524
  if (runtimeRole === 'client' && req.path.startsWith('/api/remote')) {
1509
1525
  res.status(403).json(roleGuardResponse(runtimeRole, 'hub'));
1510
1526
  return;
@@ -2767,7 +2783,7 @@ function enqueueFramePacketForClient(ws, packetOwner, meta) {
2767
2783
  }
2768
2784
  // FRAME_LANE_GENERATION_CONTRACT_END
2769
2785
 
2770
- function updateFrameSubscription(ws, payload = {}) {
2786
+ function updateFrameSubscription(ws, payload = {}) {
2771
2787
  const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
2772
2788
  ? new Set(ws.liveDeskDeviceIds)
2773
2789
  : new Set();
@@ -2800,43 +2816,43 @@ function updateFrameSubscription(ws, payload = {}) {
2800
2816
  const targetIds = newlySubscribedIds.length > 0 ? newlySubscribedIds : deviceIds;
2801
2817
  for (const deviceId of targetIds.slice(0, 80)) {
2802
2818
  startFrameSubscriptionLive(ws, 'subscribe', deviceId, { reuseExisting: true });
2803
- }
2804
- }
2805
-
2806
- function refreshFrameSubscriptionLive(ws, payload = {}) {
2807
- if (!ws || ws.readyState !== 1) {
2808
- return;
2809
- }
2810
- const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2811
- const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
2812
- const targetIds = requestedIds.length > 0
2813
- ? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
2814
- : subscribedIds;
2815
- if (targetIds.length === 0) {
2816
- return;
2817
- }
2818
- ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
2819
- payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
2820
- ));
2821
- ws.liveDeskLiveOptions = normalizeLiveOptions({
2822
- ...(ws.liveDeskLiveOptions || {}),
2823
- ...payload,
2824
- forceRestart: false,
2825
- reuseExisting: true,
2826
- monitorSelections: {
2827
- ...(ws.liveDeskLiveOptions?.monitorSelections || {}),
2828
- ...(payload.monitorSelections || {})
2829
- }
2830
- });
2831
- for (const deviceId of targetIds.slice(0, 80)) {
2832
- startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
2833
- ...ws.liveDeskLiveOptions,
2834
- reuseExisting: true
2835
- });
2836
- }
2837
- }
2838
-
2839
- function startFrameSubscriptionLive(
2819
+ }
2820
+ }
2821
+
2822
+ function refreshFrameSubscriptionLive(ws, payload = {}) {
2823
+ if (!ws || ws.readyState !== 1) {
2824
+ return;
2825
+ }
2826
+ const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2827
+ const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
2828
+ const targetIds = requestedIds.length > 0
2829
+ ? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
2830
+ : subscribedIds;
2831
+ if (targetIds.length === 0) {
2832
+ return;
2833
+ }
2834
+ ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
2835
+ payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
2836
+ ));
2837
+ ws.liveDeskLiveOptions = normalizeLiveOptions({
2838
+ ...(ws.liveDeskLiveOptions || {}),
2839
+ ...payload,
2840
+ forceRestart: false,
2841
+ reuseExisting: true,
2842
+ monitorSelections: {
2843
+ ...(ws.liveDeskLiveOptions?.monitorSelections || {}),
2844
+ ...(payload.monitorSelections || {})
2845
+ }
2846
+ });
2847
+ for (const deviceId of targetIds.slice(0, 80)) {
2848
+ startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
2849
+ ...ws.liveDeskLiveOptions,
2850
+ reuseExisting: true
2851
+ });
2852
+ }
2853
+ }
2854
+
2855
+ function startFrameSubscriptionLive(
2840
2856
  ws,
2841
2857
  reason = 'subscribe',
2842
2858
  onlyDeviceId = '',
@@ -4218,9 +4234,10 @@ app.get('/api/remote/status', (_req, res) => {
4218
4234
  runtimeRole,
4219
4235
  deviceId: runtimeDeviceId,
4220
4236
  deviceName: runtimeDeviceName,
4221
- roleSource: runtimeRoleSource,
4222
- agentPackage: '@livedesk/client',
4223
- frameLanes: snapshotFrameLaneResourceHealth(),
4237
+ roleSource: runtimeRoleSource,
4238
+ agentPackage: '@livedesk/client',
4239
+ consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
4240
+ frameLanes: snapshotFrameLaneResourceHealth(),
4224
4241
  update: getLiveDeskUpdateStatus()
4225
4242
  });
4226
4243
  });
@@ -4408,19 +4425,19 @@ app.post('/api/auth/session', async (req, res) => {
4408
4425
  res.status(400).json({ ok: false, error: normalized.error });
4409
4426
  return;
4410
4427
  }
4411
- const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
4412
- try {
4413
- const user = await verifySupabaseUser(accessToken);
4414
- const boundUserId = currentBoundHubUserId();
4415
- if (boundUserId && boundUserId !== user.id) {
4416
- res.status(403).json({ ok: false, error: 'hub-account-mismatch' });
4417
- return;
4418
- }
4419
- if (!boundUserId && !isLoopbackAddress(req.socket?.remoteAddress)) {
4420
- res.status(403).json({ ok: false, error: 'hub-first-account-binding-must-be-local' });
4421
- return;
4422
- }
4423
- runtimeAccessToken = accessToken;
4428
+ const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
4429
+ try {
4430
+ const user = await verifySupabaseUser(accessToken);
4431
+ const boundUserId = currentBoundHubUserId();
4432
+ if (boundUserId && boundUserId !== user.id) {
4433
+ res.status(403).json({ ok: false, error: 'hub-account-mismatch' });
4434
+ return;
4435
+ }
4436
+ if (!boundUserId && !isLoopbackAddress(req.socket?.remoteAddress)) {
4437
+ res.status(403).json({ ok: false, error: 'hub-first-account-binding-must-be-local' });
4438
+ return;
4439
+ }
4440
+ runtimeAccessToken = accessToken;
4424
4441
  runtimeRefreshToken = refreshToken;
4425
4442
  runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
4426
4443
  runtimeManager.setAuthenticated(true, user.id);
@@ -4430,30 +4447,31 @@ app.post('/api/auth/session', async (req, res) => {
4430
4447
  expires_at: expiresAt,
4431
4448
  user
4432
4449
  });
4433
- const hostTarget = runtimeRole === 'hub'
4434
- ? {
4435
- ok: true,
4436
- pending: true,
4437
- active: remoteHub.getStatus({ includeSecrets: false }).hostTargetActive === true
4438
- }
4439
- : { ok: true, pending: false, active: false };
4440
- const uiSession = hubUiSessionAuthority.issue(user.id);
4441
- res.setHeader('Set-Cookie', serializeHubUiSessionCookie(uiSession.sessionId, uiSession.expiresAt, {
4442
- secure: isSecureHubHttpRequest(req)
4443
- }));
4444
- res.json({
4445
- ok: true,
4446
- authenticated: true,
4447
- persisted,
4448
- userId: user.id,
4449
- role: runtimeRole,
4450
- hostTarget,
4451
- uiSession: {
4452
- csrfToken: uiSession.csrfToken,
4453
- expiresAt: uiSession.expiresAt
4454
- }
4450
+ const hostTarget = runtimeRole === 'hub'
4451
+ ? {
4452
+ ok: true,
4453
+ pending: true,
4454
+ active: remoteHub.getStatus({ includeSecrets: false }).hostTargetActive === true
4455
+ }
4456
+ : { ok: true, pending: false, active: false };
4457
+ const uiSession = hubUiSessionAuthority.issue(user.id);
4458
+ res.setHeader('Set-Cookie', serializeHubUiSessionCookie(uiSession.sessionId, uiSession.expiresAt, {
4459
+ secure: isSecureHubHttpRequest(req)
4460
+ }));
4461
+ res.json({
4462
+ ok: true,
4463
+ authenticated: true,
4464
+ persisted,
4465
+ userId: user.id,
4466
+ role: runtimeRole,
4467
+ hostTarget,
4468
+ uiSession: {
4469
+ csrfToken: uiSession.csrfToken,
4470
+ expiresAt: uiSession.expiresAt
4471
+ }
4455
4472
  });
4456
4473
  if (runtimeRole === 'hub') {
4474
+ hubConsoleRelay?.refresh();
4457
4475
  scheduleAuthenticatedHubHostTargetPublication('session-received');
4458
4476
  }
4459
4477
  } catch (error) {
@@ -4475,10 +4493,11 @@ function getHubHostTargetLeaseStatus() {
4475
4493
  ...hubHostTargetLeaseState,
4476
4494
  renewing: hubHostTargetRenewInFlight,
4477
4495
  renewalIntervalMs: HUB_HOST_TARGET_RENEW_MS,
4478
- leaseDurationMs: HUB_HOST_TARGET_LEASE_MS,
4479
- authenticated: Boolean(runtimeAccessToken),
4480
- timerActive: Boolean(hubHostTargetRenewTimer),
4481
- wake: { ...hubWakeNotificationState }
4496
+ leaseDurationMs: HUB_HOST_TARGET_LEASE_MS,
4497
+ authenticated: Boolean(runtimeAccessToken),
4498
+ timerActive: Boolean(hubHostTargetRenewTimer),
4499
+ wake: { ...hubWakeNotificationState },
4500
+ consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' }
4482
4501
  };
4483
4502
  }
4484
4503
 
@@ -4741,36 +4760,36 @@ async function clearHubHostTarget(reason = 'shutdown') {
4741
4760
  return { ok: !error, error, lease: getHubHostTargetLeaseStatus() };
4742
4761
  }
4743
4762
 
4744
- function startHubHostTargetLeaseRenewal() {
4763
+ function startHubHostTargetLeaseRenewal() {
4745
4764
  if (hubHostTargetRenewTimer || runtimeRole !== 'hub') {
4746
4765
  return;
4747
4766
  }
4748
4767
  hubHostTargetRenewTimer = setInterval(() => {
4749
4768
  void publishHubHostTargetWithPendingRoleTakeover('renewal');
4750
4769
  }, HUB_HOST_TARGET_RENEW_MS);
4751
- hubHostTargetRenewTimer.unref?.();
4752
- }
4753
-
4754
- function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-received') {
4755
- startHubHostTargetLeaseRenewal();
4756
- setImmediate(() => {
4757
- void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
4758
- const message = error instanceof Error ? error.message : String(error);
4759
- updateHubHostTargetLeaseState({ state: 'error', lastError: message });
4760
- console.error(`[LiveDesk Hub] Host target ${reason} background publish failed: ${message}`);
4761
- });
4762
- });
4763
- }
4770
+ hubHostTargetRenewTimer.unref?.();
4771
+ }
4764
4772
 
4765
- app.delete('/api/auth/session', async (req, res) => {
4773
+ function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-received') {
4774
+ startHubHostTargetLeaseRenewal();
4775
+ setImmediate(() => {
4776
+ void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
4777
+ const message = error instanceof Error ? error.message : String(error);
4778
+ updateHubHostTargetLeaseState({ state: 'error', lastError: message });
4779
+ console.error(`[LiveDesk Hub] Host target ${reason} background publish failed: ${message}`);
4780
+ });
4781
+ });
4782
+ }
4783
+
4784
+ app.delete('/api/auth/session', async (req, res) => {
4766
4785
  noStore(res);
4767
4786
  const hostTarget = runtimeRole === 'hub'
4768
4787
  ? await clearHubHostTarget('logout')
4769
4788
  : { ok: true, active: false };
4770
- clearRuntimeSession();
4771
- res.setHeader('Set-Cookie', clearHubUiSessionCookie({ secure: isSecureHubHttpRequest(req) }));
4772
- res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
4773
- });
4789
+ clearRuntimeSession();
4790
+ res.setHeader('Set-Cookie', clearHubUiSessionCookie({ secure: isSecureHubHttpRequest(req) }));
4791
+ res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
4792
+ });
4774
4793
 
4775
4794
  app.get('/api/hub/status', (_req, res) => {
4776
4795
  noStore(res);
@@ -4782,10 +4801,11 @@ app.get('/api/hub/status', (_req, res) => {
4782
4801
  ...remoteHub.getStatus({ includeSecrets: false }),
4783
4802
  role: 'hub',
4784
4803
  deviceId: runtimeDeviceId,
4785
- deviceName: runtimeDeviceName,
4786
- roleSource: runtimeRoleSource,
4787
- runtimeStarted: true,
4788
- hostTargetLease: getHubHostTargetLeaseStatus(),
4804
+ deviceName: runtimeDeviceName,
4805
+ roleSource: runtimeRoleSource,
4806
+ runtimeStarted: true,
4807
+ consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
4808
+ hostTargetLease: getHubHostTargetLeaseStatus(),
4789
4809
  update: getLiveDeskUpdateStatus()
4790
4810
  });
4791
4811
  });
@@ -5571,20 +5591,20 @@ if (existsSync(webIndexPath)) {
5571
5591
  });
5572
5592
  }
5573
5593
 
5574
- httpServer.on('upgrade', (req, socket, head) => {
5575
- if (!isTrustedBrowserRequest(req)) {
5576
- socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
5577
- socket.destroy();
5578
- return;
5579
- }
5580
- const authorization = authorizeHubUiRequest(req);
5581
- if (!authorization.ok) {
5582
- const label = authorization.status === 403 ? 'Forbidden' : 'Unauthorized';
5583
- socket.write(`HTTP/1.1 ${authorization.status} ${label}\r\nConnection: close\r\n\r\n`);
5584
- socket.destroy();
5585
- return;
5586
- }
5587
- try {
5594
+ httpServer.on('upgrade', (req, socket, head) => {
5595
+ if (!isTrustedBrowserRequest(req)) {
5596
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
5597
+ socket.destroy();
5598
+ return;
5599
+ }
5600
+ const authorization = authorizeHubUiRequest(req);
5601
+ if (!authorization.ok) {
5602
+ const label = authorization.status === 403 ? 'Forbidden' : 'Unauthorized';
5603
+ socket.write(`HTTP/1.1 ${authorization.status} ${label}\r\nConnection: close\r\n\r\n`);
5604
+ socket.destroy();
5605
+ return;
5606
+ }
5607
+ try {
5588
5608
  const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
5589
5609
  if (parsed.pathname === '/api/remote/frames/ws') {
5590
5610
  frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
@@ -5631,11 +5651,11 @@ frameWss.on('connection', (ws, req) => {
5631
5651
  ws.on('message', data => {
5632
5652
  try {
5633
5653
  const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
5634
- if (payload?.type === 'subscribe') {
5635
- updateFrameSubscription(ws, payload);
5636
- } else if (payload?.type === 'refresh-live') {
5637
- refreshFrameSubscriptionLive(ws, payload);
5638
- } else if (payload?.type === 'restart-live') {
5654
+ if (payload?.type === 'subscribe') {
5655
+ updateFrameSubscription(ws, payload);
5656
+ } else if (payload?.type === 'refresh-live') {
5657
+ refreshFrameSubscriptionLive(ws, payload);
5658
+ } else if (payload?.type === 'restart-live') {
5639
5659
  restartFrameSubscriptionLive(ws, payload);
5640
5660
  }
5641
5661
  } catch {
@@ -5792,13 +5812,14 @@ hubSharedFolders.startAutoSync(
5792
5812
  () => remoteHub.listDevices({ includeDataUrl: false }).filter(device => device.connected).map(device => device.deviceId),
5793
5813
  () => process.env.LIVEDESK_REMOTE_DIRECTORY || 'Desktop/LiveDeskFiles'
5794
5814
  );
5795
- httpServer.listen(httpPort, httpHost, () => {
5815
+ httpServer.listen(httpPort, httpHost, () => {
5796
5816
  const status = remoteHub.getStatus({ includeSecrets: true });
5797
5817
  const managerVersion = String(process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version || 'dev');
5798
5818
  console.log(`[LiveDesk Hub] Version ${managerVersion}`);
5799
- console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
5800
- console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
5801
- });
5819
+ console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
5820
+ console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
5821
+ hubConsoleRelay?.start();
5822
+ });
5802
5823
 
5803
5824
  const roleWatchTimer = runtimeRole === 'hub'
5804
5825
  ? setInterval(() => { void watchAuthoritativeRuntimeRole(); }, 5000)
@@ -5879,9 +5900,10 @@ function shutdownHub(signal) {
5879
5900
  const startedAt = Date.now();
5880
5901
  console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
5881
5902
  if (roleWatchTimer) clearInterval(roleWatchTimer);
5882
- clearInterval(browserWebSocketHeartbeatTimer);
5883
- runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
5884
- atlasClients.clear();
5903
+ clearInterval(browserWebSocketHeartbeatTimer);
5904
+ runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
5905
+ runSynchronousShutdownStep('mobile console relay close', () => hubConsoleRelay?.close());
5906
+ atlasClients.clear();
5885
5907
  runSynchronousShutdownStep('shared folder close', () => hubSharedFolders.close());
5886
5908
 
5887
5909
  const httpClosed = new Promise(resolveClose => {
@@ -1,16 +1,16 @@
1
- import { effectiveDevicePolicy } from './settings-schema.js';
2
-
3
- export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
4
- const policy = effectiveDevicePolicy(settings);
5
- return {
6
- ...policy,
7
- deviceId: String(deviceId || ''),
8
- supported: {
9
- control: capabilities.control !== false,
10
- clipboardText: capabilities.clipboardText !== false,
11
- fileTransfer: capabilities.fileTransfer !== false,
12
- remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
13
- agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
14
- }
15
- };
16
- }
1
+ import { effectiveDevicePolicy } from './settings-schema.js';
2
+
3
+ export function buildEffectiveDevicePolicy(settings, { deviceId = '', capabilities = {} } = {}) {
4
+ const policy = effectiveDevicePolicy(settings);
5
+ return {
6
+ ...policy,
7
+ deviceId: String(deviceId || ''),
8
+ supported: {
9
+ control: capabilities.control !== false,
10
+ clipboardText: capabilities.clipboardText !== false,
11
+ fileTransfer: capabilities.fileTransfer !== false,
12
+ remoteAudio: capabilities.audio === true || capabilities.remoteAudio === true,
13
+ agent: Array.isArray(capabilities.agentTools) && capabilities.agentTools.length > 0
14
+ }
15
+ };
16
+ }