@livedesk/hub 0.1.54 → 0.1.56

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.54",
3
+ "version": "0.1.56",
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",
@@ -1,7 +1,8 @@
1
1
  import WebSocket from 'ws';
2
2
 
3
- const DEFAULT_CONSOLE_RELAY_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
4
- const DEFAULT_RETRY_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 20_000];
3
+ const DEFAULT_CONSOLE_RELAY_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
4
+ const DEFAULT_RETRY_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 20_000, 60_000];
5
+ const HTTP_429_RETRY_DELAY_MS = 60_000;
5
6
  const MAX_CHANNELS = 32;
6
7
  const MAX_HTTP_REQUEST_BYTES = 1024 * 1024;
7
8
  const MAX_HTTP_RESPONSE_BYTES = 4 * 1024 * 1024;
@@ -103,9 +104,12 @@ export function createHubConsoleRelay(options = {}) {
103
104
  let generation = 0;
104
105
  let stopped = true;
105
106
  let state = relayUrl ? 'idle' : 'disabled';
106
- let lastConnectedAt = '';
107
- let lastError = relayUrl instanceof URL ? '' : relayUrl === '' ? '' : 'invalid-relay-url';
108
- let droppedBinaryMessages = 0;
107
+ let lastConnectedAt = '';
108
+ let lastError = relayUrl instanceof URL ? '' : relayUrl === '' ? '' : 'invalid-relay-url';
109
+ let lastHttpStatus = 0;
110
+ let retryNotBeforeAt = 0;
111
+ let nextRetryAt = '';
112
+ let droppedBinaryMessages = 0;
109
113
 
110
114
  const sendRelay = (payload, bypassBackpressure = false) => {
111
115
  if (!relaySocket || relaySocket.readyState !== WebSocketImpl.OPEN) return false;
@@ -265,11 +269,15 @@ export function createHubConsoleRelay(options = {}) {
265
269
  const handleMessage = raw => {
266
270
  const payload = parseMessage(raw);
267
271
  if (!payload?.type) return;
268
- if (payload.type === 'relay-ready') {
269
- state = 'connected';
270
- lastConnectedAt = new Date().toISOString();
271
- lastError = '';
272
- return;
272
+ if (payload.type === 'relay-ready') {
273
+ state = 'connected';
274
+ lastConnectedAt = new Date().toISOString();
275
+ lastError = '';
276
+ lastHttpStatus = 0;
277
+ retryAttempt = 0;
278
+ retryNotBeforeAt = 0;
279
+ nextRetryAt = '';
280
+ return;
273
281
  }
274
282
  if (payload.type === 'console-detached') {
275
283
  closeConsoleChannels(String(payload.consoleId || ''));
@@ -294,22 +302,27 @@ export function createHubConsoleRelay(options = {}) {
294
302
  }
295
303
  };
296
304
 
297
- const scheduleReconnect = connect => {
298
- if (stopped || retryTimer || !(relayUrl instanceof URL)) return;
299
- const delayMs = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
300
- retryAttempt += 1;
301
- state = 'waiting-retry';
302
- retryTimer = setTimeout(() => {
303
- retryTimer = null;
304
- void connect();
305
+ const scheduleReconnect = connect => {
306
+ if (stopped || retryTimer || !(relayUrl instanceof URL)) return;
307
+ const sequenceDelayMs = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
308
+ const delayMs = Math.max(sequenceDelayMs, retryNotBeforeAt - Date.now(), 0);
309
+ retryAttempt += 1;
310
+ state = 'waiting-retry';
311
+ nextRetryAt = new Date(Date.now() + delayMs).toISOString();
312
+ retryTimer = setTimeout(() => {
313
+ retryTimer = null;
314
+ nextRetryAt = '';
315
+ void connect();
305
316
  }, delayMs);
306
317
  retryTimer.unref?.();
307
318
  };
308
319
 
309
320
  const connect = async () => {
310
321
  if (stopped || !(relayUrl instanceof URL) || !deviceId) return;
311
- const ownerGeneration = ++generation;
312
- state = 'connecting';
322
+ const ownerGeneration = ++generation;
323
+ let ownerHttpStatus = 0;
324
+ state = 'connecting';
325
+ nextRetryAt = '';
313
326
  let accessToken = '';
314
327
  try {
315
328
  accessToken = String(await getAccessToken() || '').trim();
@@ -339,25 +352,46 @@ export function createHubConsoleRelay(options = {}) {
339
352
  try { socket.terminate?.(); } catch { /* stale connect is already gone */ }
340
353
  return;
341
354
  }
342
- relaySocket = socket;
343
- socket.once('open', () => {
344
- if (relaySocket !== socket || ownerGeneration !== generation || stopped) return;
345
- retryAttempt = 0;
346
- state = 'authenticating';
347
- });
355
+ relaySocket = socket;
356
+ socket.once('open', () => {
357
+ if (relaySocket !== socket || ownerGeneration !== generation || stopped) return;
358
+ state = 'authenticating';
359
+ });
360
+ socket.once('unexpected-response', (_request, response) => {
361
+ if (ownerGeneration !== generation || stopped) {
362
+ response.resume?.();
363
+ return;
364
+ }
365
+ const status = Math.max(0, Number(response?.statusCode || 0));
366
+ ownerHttpStatus = status;
367
+ lastHttpStatus = status;
368
+ lastError = status > 0
369
+ ? `Console relay returned HTTP ${status}`
370
+ : 'Console relay returned an unexpected response.';
371
+ if (status === 429) {
372
+ retryNotBeforeAt = Math.max(retryNotBeforeAt, Date.now() + HTTP_429_RETRY_DELAY_MS);
373
+ }
374
+ if (relaySocket === socket) relaySocket = null;
375
+ response.resume?.();
376
+ try { socket.terminate?.(); } catch { /* retry owns the rejected upgrade */ }
377
+ scheduleReconnect(connect);
378
+ });
348
379
  socket.on('message', raw => {
349
380
  if (relaySocket === socket && ownerGeneration === generation && !stopped) handleMessage(raw);
350
381
  });
351
- socket.once('close', () => {
352
- if (relaySocket === socket) relaySocket = null;
353
- if (ownerGeneration !== generation || stopped) return;
354
- state = 'disconnected';
355
- closeAllLocalSockets('relay-disconnected');
356
- scheduleReconnect(connect);
382
+ socket.once('close', () => {
383
+ if (relaySocket === socket) relaySocket = null;
384
+ if (ownerGeneration !== generation || stopped) return;
385
+ if (!retryTimer) state = 'disconnected';
386
+ closeAllLocalSockets('relay-disconnected');
387
+ scheduleReconnect(connect);
357
388
  });
358
- socket.once('error', error => {
359
- lastError = error instanceof Error ? error.message : String(error);
360
- try { socket.terminate?.(); } catch { /* close handler owns retry */ }
389
+ socket.once('error', error => {
390
+ if (ownerHttpStatus <= 0) {
391
+ lastHttpStatus = 0;
392
+ lastError = error instanceof Error ? error.message : String(error);
393
+ }
394
+ try { socket.terminate?.(); } catch { /* close handler owns retry */ }
361
395
  });
362
396
  };
363
397
 
@@ -373,10 +407,13 @@ export function createHubConsoleRelay(options = {}) {
373
407
  return;
374
408
  }
375
409
  generation += 1;
376
- if (retryTimer) {
410
+ if (retryTimer) {
377
411
  clearTimeout(retryTimer);
378
412
  retryTimer = null;
379
- }
413
+ }
414
+ retryAttempt = 0;
415
+ retryNotBeforeAt = 0;
416
+ nextRetryAt = '';
380
417
  const socket = relaySocket;
381
418
  relaySocket = null;
382
419
  try { socket?.terminate?.(); } catch { /* refresh owns the exact socket */ }
@@ -389,7 +426,8 @@ export function createHubConsoleRelay(options = {}) {
389
426
  stopped = true;
390
427
  generation += 1;
391
428
  if (retryTimer) clearTimeout(retryTimer);
392
- retryTimer = null;
429
+ retryTimer = null;
430
+ nextRetryAt = '';
393
431
  closeAllLocalSockets('hub-shutdown');
394
432
  const socket = relaySocket;
395
433
  relaySocket = null;
@@ -408,9 +446,12 @@ export function createHubConsoleRelay(options = {}) {
408
446
  connected: state === 'connected',
409
447
  localWebSocketChannels: localSockets.size,
410
448
  pendingHttpRequests: pendingHttp.size,
411
- droppedBinaryMessages,
412
- lastConnectedAt,
413
- lastError
449
+ droppedBinaryMessages,
450
+ lastConnectedAt,
451
+ lastError,
452
+ lastHttpStatus,
453
+ retryAttempt,
454
+ nextRetryAt
414
455
  })
415
456
  };
416
457
  }
@@ -420,5 +461,5 @@ export const consoleRelayContract = Object.freeze({
420
461
  maxHttpRequestBytes: MAX_HTTP_REQUEST_BYTES,
421
462
  maxHttpResponseBytes: MAX_HTTP_RESPONSE_BYTES,
422
463
  maxBufferedBytes: MAX_RELAY_BUFFERED_BYTES,
423
- binaryHeaderBytes: BINARY_HEADER_BYTES
424
- });
464
+ binaryHeaderBytes: BINARY_HEADER_BYTES
465
+ });
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,
@@ -2397,9 +2404,12 @@ export function createRemoteHub(options = {}) {
2397
2404
  const hostInstanceId = safeString(options.hostInstanceId ?? env.LIVEDESK_HUB_INSTANCE_ID ?? env.MINDEXEC_BRIDGE_INSTANCE_ID ?? crypto.randomUUID(), 128) || crypto.randomUUID();
2398
2405
  const publicEndpoint = safeString(env.LIVEDESK_REMOTE_PUBLIC_ENDPOINT || env.MINDEXEC_REMOTE_PUBLIC_ENDPOINT || env.REMOTE_HUB_PUBLIC_ENDPOINT, 256);
2399
2406
  const publicHost = safeString(env.LIVEDESK_REMOTE_PUBLIC_HOST || env.MINDEXEC_REMOTE_PUBLIC_HOST || env.REMOTE_HUB_PUBLIC_HOST, 128);
2400
- const pairToken = safeString(
2401
- options.pairToken || env.REMOTE_HUB_PAIR_TOKEN || env.LIVEDESK_CLIENT_PAIR_TOKEN || env.MINDEXEC_REMOTE_PAIR_TOKEN || crypto.randomBytes(6).toString('hex'),
2402
- 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);
2403
2413
  const relayControl = options.relayControl && typeof options.relayControl === 'object'
2404
2414
  ? options.relayControl
2405
2415
  : createHubRelayControl({
@@ -7949,10 +7959,24 @@ export function createRemoteHub(options = {}) {
7949
7959
  function handleAgentMessage(socket, state, message) {
7950
7960
  if (!message || typeof message !== 'object') {
7951
7961
  return;
7952
- }
7953
-
7954
- if (!state.authenticated) {
7955
- 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) {
7956
7980
  message = {
7957
7981
  ...message,
7958
7982
  capabilities: {
@@ -7988,7 +8012,7 @@ export function createRemoteHub(options = {}) {
7988
8012
  return;
7989
8013
  }
7990
8014
 
7991
- const channel = safeString(message.channel || message.Channel, 40).toLowerCase();
8015
+ const channel = safeString(message.channel || message.Channel, 40).toLowerCase() || 'control';
7992
8016
  if (channel === 'input') {
7993
8017
  const device = attachInputSocket(socket, message);
7994
8018
  if (!device) {
@@ -8538,18 +8562,60 @@ export function createRemoteHub(options = {}) {
8538
8562
  if (firstChunk) {
8539
8563
  processData(firstChunk);
8540
8564
  }
8541
- }
8542
-
8543
- function handleSocket(socket) {
8544
- socket.once('data', chunk => {
8545
- const firstChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
8546
- if (isRemoteHubWebSocketUpgradeStart(firstChunk)) {
8547
- handleWebSocketUpgradeSocket(socket, firstChunk);
8548
- return;
8549
- }
8550
-
8551
- handleTcpAgentSocket(socket, firstChunk);
8552
- });
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
+ });
8553
8619
 
8554
8620
  socket.once('error', () => {
8555
8621
  // The transport-specific handler owns logging after the first byte.
@@ -8571,10 +8637,10 @@ export function createRemoteHub(options = {}) {
8571
8637
  started = true;
8572
8638
  boundPort = candidateServer.address()?.port || port;
8573
8639
  lastError = '';
8574
- logEvent('remote', `VuvoDesk Hub client endpoint listening on tcp://${host}:${boundPort}`, 'success');
8575
- if (host === '0.0.0.0' || host === '::') {
8576
- logWarn('remote', 'VuvoDesk Hub client endpoint is externally reachable. Use a strong pairing token and trusted network.');
8577
- }
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
+ }
8578
8644
  emitRemoteEvent('RemoteHubStarted', null);
8579
8645
  resolve();
8580
8646
  });
package/src/server.js CHANGED
@@ -4342,27 +4342,35 @@ app.post('/api/settings/agent/summary', async (req, res) => {
4342
4342
  }
4343
4343
  });
4344
4344
 
4345
- app.get('/api/remote/status', (_req, res) => {
4346
- noStore(res);
4347
- if (runtimeRole !== 'hub') {
4345
+ app.get('/api/remote/status', async (_req, res) => {
4346
+ noStore(res);
4347
+ if (runtimeRole !== 'hub') {
4348
4348
  res.status(403).json({ ok: false, error: 'role-not-allowed' });
4349
4349
  return;
4350
4350
  }
4351
- const secretStatus = remoteHub.getStatus({ includeSecrets: true });
4352
- res.json({
4353
- ...remoteHub.getStatus({ includeSecrets: false }),
4354
- pairingPin: secretStatus.pairingPin,
4355
- product: 'LiveDesk',
4356
- runtimeRole,
4357
- deviceId: runtimeDeviceId,
4358
- deviceName: runtimeDeviceName,
4359
- roleSource: runtimeRoleSource,
4360
- agentPackage: '@livedesk/client',
4361
- consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
4362
- frameLanes: snapshotFrameLaneResourceHealth(),
4363
- update: getLiveDeskUpdateStatus()
4364
- });
4365
- });
4351
+ try {
4352
+ const [secretStatus, wallPreferences] = await Promise.all([
4353
+ Promise.resolve(remoteHub.getStatus({ includeSecrets: true })),
4354
+ liveDeskSettingsStore.getWallPreferencesRecord()
4355
+ ]);
4356
+ res.json({
4357
+ ...remoteHub.getStatus({ includeSecrets: false }),
4358
+ pairingPin: secretStatus.pairingPin,
4359
+ product: 'LiveDesk',
4360
+ runtimeRole,
4361
+ deviceId: runtimeDeviceId,
4362
+ deviceName: runtimeDeviceName,
4363
+ roleSource: runtimeRoleSource,
4364
+ agentPackage: '@livedesk/client',
4365
+ wallPreferences,
4366
+ consoleRelay: hubConsoleRelay?.inspect() || { enabled: false, state: 'starting' },
4367
+ frameLanes: snapshotFrameLaneResourceHealth(),
4368
+ update: getLiveDeskUpdateStatus()
4369
+ });
4370
+ } catch (error) {
4371
+ res.status(500).json({ ok: false, error: error instanceof Error ? error.message : String(error) });
4372
+ }
4373
+ });
4366
4374
 
4367
4375
  app.post('/api/remote/diagnostics/transport/start', async (req, res) => {
4368
4376
  noStore(res);
@@ -46,9 +46,11 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
46
46
  rememberLastMonitor: true,
47
47
  keepControlReadyBetweenPages: true
48
48
  },
49
- wall: {
50
- performanceMode: 'auto',
51
- autoStart: true,
49
+ wall: {
50
+ performanceMode: 'auto',
51
+ cadence: 'fast',
52
+ viewScale: 100,
53
+ autoStart: true,
52
54
  connectedOnly: false,
53
55
  keepEmptySlots: true,
54
56
  showDeviceStatus: true,
@@ -110,9 +112,10 @@ export const DEFAULT_LIVEDESK_SETTINGS = Object.freeze({
110
112
  }
111
113
  });
112
114
 
113
- const ENUMS = {
115
+ const ENUMS = {
114
116
  accessMode: new Set(['trusted-only', 'ask-every-time', 'view-only', 'block-remote-access']),
115
- performanceMode: new Set(['auto', 'responsive', 'quality', 'custom']),
117
+ performanceMode: new Set(['auto', 'responsive', 'quality', 'custom']),
118
+ wallCadence: new Set(['slow', 'fast']),
116
119
  permissionMode: new Set(['ask', 'safe-auto', 'full-access', 'custom']),
117
120
  wallFrameMode: new Set(['auto', 'mode2-lzo', 'mode3-h264-hw', 'mode4-h264-atlas']),
118
121
  controlFrameMode: new Set(['mode3-h264-hw', 'mode5-lzo-delta']),
@@ -169,9 +172,11 @@ const RULES = {
169
172
  control: {
170
173
  ...bools(['allowKeyboardMouse', 'allowSystemShortcuts', 'allowClipboardText', 'allowRemoteRestart', 'reconnectAfterRemoteRestart', 'allowSwitchingMonitors', 'showConnectionToolbar', 'showRemoteCursor', 'openControlOnDoubleClick', 'startRemoteAudioWithControl', 'fitRemoteScreen', 'rememberLastMonitor', 'keepControlReadyBetweenPages'])
171
174
  },
172
- wall: {
173
- performanceMode: { type: 'enum', values: ENUMS.performanceMode },
174
- ...bools(['autoStart', 'connectedOnly', 'keepEmptySlots', 'showDeviceStatus', 'showPerformanceDetails', 'pauseHiddenTiles', 'reduceWhenHidden', 'autoAdjustTileQuality', 'rememberDevicePositions'])
175
+ wall: {
176
+ performanceMode: { type: 'enum', values: ENUMS.performanceMode },
177
+ cadence: { type: 'enum', values: ENUMS.wallCadence },
178
+ viewScale: { type: 'number', min: 0, max: 100 },
179
+ ...bools(['autoStart', 'connectedOnly', 'keepEmptySlots', 'showDeviceStatus', 'showPerformanceDetails', 'pauseHiddenTiles', 'reduceWhenHidden', 'autoAdjustTileQuality', 'rememberDevicePositions'])
175
180
  },
176
181
  filesAudio: {
177
182
  ...bools(['allowFileTransfer', 'allowFolderSync', 'askBeforeReceivingFiles', 'openReceivedFolder', 'notifyTransferComplete', 'allowOverwrite', 'allowRemoteAudio', 'startAudioMuted', 'rememberVolume', 'automaticallyRecoverAudio', 'showAudioTroubleshooting', 'rollingBufferEnabled', 'includeRemoteCursor']),
@@ -40,12 +40,22 @@ export class LiveDeskSettingsStore {
40
40
  return structuredClone(this.record);
41
41
  }
42
42
 
43
- getCached() {
44
- if (!this.record) return DEFAULT_LIVEDESK_SETTINGS;
45
- return { ...this.record.settings, revision: this.record.revision };
46
- }
47
-
48
- async get() {
43
+ getCached() {
44
+ if (!this.record) return DEFAULT_LIVEDESK_SETTINGS;
45
+ return { ...this.record.settings, revision: this.record.revision };
46
+ }
47
+
48
+ async getWallPreferencesRecord() {
49
+ if (!this.record) await this.getRecord();
50
+ return {
51
+ revision: this.record.revision,
52
+ updatedAt: this.record.updatedAt,
53
+ cadence: this.record.settings.wall.cadence,
54
+ viewScale: this.record.settings.wall.viewScale
55
+ };
56
+ }
57
+
58
+ async get() {
49
59
  return publicSettings((await this.getRecord()).settings);
50
60
  }
51
61