@livedesk/hub 0.1.55 → 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 +1 -1
- package/src/console-relay.js +84 -43
- package/src/server.js +26 -18
- package/src/settings/settings-schema.js +13 -8
- package/src/settings/settings-store.js +16 -6
package/package.json
CHANGED
package/src/console-relay.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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/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
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|