livedesk 0.1.554 → 0.1.563

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.
@@ -19,7 +19,8 @@ import {
19
19
  signalAgentTree
20
20
  } from '../src/runtime/agent-process-lifecycle.js';
21
21
  import { writeWindowsOwnedProcessManifest } from '../src/runtime/windows-owned-process-manifest.js';
22
- import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
22
+ import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
23
+ import { resolveSpawnablePackagedPath } from '../src/runtime/packaged-executable-path.js';
23
24
  import {
24
25
  inspectLinuxVideoAcceleration,
25
26
  installLinuxVideoAcceleration
@@ -4428,14 +4429,14 @@ function getFastRuntime() {
4428
4429
  const arch = os.arch();
4429
4430
  const resolvePackagedRuntime = (packageName, rid, executableName) => {
4430
4431
  try {
4431
- const packagePath = require.resolve(`${packageName}/package.json`);
4432
- const fastRoot = join(dirname(packagePath), 'fast');
4433
- return {
4434
- rid,
4435
- packageName,
4436
- executable: join(fastRoot, executableName),
4437
- dll: join(fastRoot, 'livedesk-client-fast.dll')
4438
- };
4432
+ const packagePath = require.resolve(`${packageName}/package.json`);
4433
+ const fastRoot = join(dirname(packagePath), 'fast');
4434
+ return {
4435
+ rid,
4436
+ packageName,
4437
+ executable: resolveSpawnablePackagedPath(join(fastRoot, executableName)),
4438
+ dll: resolveSpawnablePackagedPath(join(fastRoot, 'livedesk-client-fast.dll'))
4439
+ };
4439
4440
  } catch {
4440
4441
  return {
4441
4442
  rid,
@@ -4570,13 +4571,15 @@ function clearFastPreflightCache() {
4570
4571
  }
4571
4572
  }
4572
4573
 
4573
- function resolveBundledFfmpegPaths() {
4574
- const paths = [];
4575
- const addPath = (candidate) => {
4576
- const ffmpegPath = String(candidate || '').trim();
4577
- if (ffmpegPath && existsSync(ffmpegPath) && !paths.includes(ffmpegPath)) {
4578
- paths.push(ffmpegPath);
4579
- }
4574
+ function resolveBundledFfmpegPaths() {
4575
+ const paths = [];
4576
+ const addPath = (candidate) => {
4577
+ // Electron can resolve ffmpeg-static through app.asar, but RemoteFast
4578
+ // is a native process and must receive the physical unpacked path.
4579
+ const ffmpegPath = resolveSpawnablePackagedPath(String(candidate || '').trim());
4580
+ if (ffmpegPath && existsSync(ffmpegPath) && !paths.includes(ffmpegPath)) {
4581
+ paths.push(ffmpegPath);
4582
+ }
4580
4583
  };
4581
4584
 
4582
4585
  const addFfmpegInstaller = () => {
@@ -0,0 +1,23 @@
1
+ import { existsSync } from 'node:fs';
2
+
3
+ const ASAR_PATH_SEGMENT = /([\\/])app\.asar([\\/])/i;
4
+
5
+ /**
6
+ * Electron's Node loader can read JavaScript through the virtual app.asar
7
+ * path, but the operating system cannot spawn an executable from that virtual
8
+ * archive. electron-builder places executable payloads in the matching
9
+ * app.asar.unpacked tree, so only spawn targets cross that boundary.
10
+ */
11
+ export function resolveSpawnablePackagedPath(value, pathExists = existsSync) {
12
+ const candidate = String(value || '');
13
+ if (!candidate || !ASAR_PATH_SEGMENT.test(candidate)) {
14
+ return candidate;
15
+ }
16
+
17
+ const unpacked = candidate.replace(ASAR_PATH_SEGMENT, '$1app.asar.unpacked$2');
18
+ try {
19
+ return pathExists(unpacked) ? unpacked : candidate;
20
+ } catch {
21
+ return candidate;
22
+ }
23
+ }
@@ -1,9 +1,10 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.yml
2
2
  files:
3
3
  - bin/**
4
- - bootstrap/**
5
- - client/**
6
- - electron/**
4
+ - bootstrap/**
5
+ - client/**
6
+ - diagnostics/**
7
+ - electron/**
7
8
  - hub/**
8
9
  - runtime-core/**
9
10
  - web/**
@@ -14,9 +15,10 @@ files:
14
15
  - "!client/fast/osx-arm64/**"
15
16
  asarUnpack:
16
17
  - bin/**
17
- - bootstrap/**
18
- - client/**
19
- - electron/icon.*
18
+ - bootstrap/**
19
+ - client/**
20
+ - diagnostics/**
21
+ - electron/icon.*
20
22
  - electron/tray.*
21
23
  - hub/**
22
24
  - runtime-core/**
@@ -1,9 +1,10 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.yml
2
2
  files:
3
3
  - bin/**
4
- - bootstrap/**
5
- - client/**
6
- - electron/**
4
+ - bootstrap/**
5
+ - client/**
6
+ - diagnostics/**
7
+ - electron/**
7
8
  - hub/**
8
9
  - runtime-core/**
9
10
  - web/**
@@ -14,9 +15,10 @@ files:
14
15
  - "!client/fast/osx-x64/**"
15
16
  asarUnpack:
16
17
  - bin/**
17
- - bootstrap/**
18
- - client/**
19
- - electron/icon.*
18
+ - bootstrap/**
19
+ - client/**
20
+ - diagnostics/**
21
+ - electron/icon.*
20
22
  - electron/tray.*
21
23
  - hub/**
22
24
  - runtime-core/**
@@ -1,9 +1,10 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.yml
2
2
  files:
3
3
  - bin/**
4
- - bootstrap/**
5
- - client/**
6
- - electron/**
4
+ - bootstrap/**
5
+ - client/**
6
+ - diagnostics/**
7
+ - electron/**
7
8
  - hub/**
8
9
  - runtime-core/**
9
10
  - web/**
@@ -14,9 +15,10 @@ files:
14
15
  - "!client/fast/osx-arm64/**"
15
16
  asarUnpack:
16
17
  - bin/**
17
- - bootstrap/**
18
- - client/**
19
- - electron/icon.*
18
+ - bootstrap/**
19
+ - client/**
20
+ - diagnostics/**
21
+ - electron/icon.*
20
22
  - electron/tray.*
21
23
  - hub/**
22
24
  - runtime-core/**
@@ -0,0 +1,9 @@
1
+ extends: file:packages/livedesk/electron/electron-builder.win.yml
2
+ artifactName: LiveDesk-${version}-dev-${arch}.${ext}
3
+ directories:
4
+ output: dist/desktop-dev
5
+ extraMetadata:
6
+ desktopUpdateChannel: dev
7
+ publish:
8
+ provider: generic
9
+ url: https://livedesk-desktop-updates.lovecrdm.workers.dev/dev/windows/x64
@@ -1,9 +1,10 @@
1
1
  extends: file:packages/livedesk/electron/electron-builder.yml
2
2
  files:
3
3
  - bin/**
4
- - bootstrap/**
5
- - client/**
6
- - electron/**
4
+ - bootstrap/**
5
+ - client/**
6
+ - diagnostics/**
7
+ - electron/**
7
8
  - hub/**
8
9
  - runtime-core/**
9
10
  - web/**
@@ -14,9 +15,10 @@ files:
14
15
  - "!client/fast/osx-arm64/**"
15
16
  asarUnpack:
16
17
  - bin/**
17
- - bootstrap/**
18
- - client/**
19
- - electron/icon.*
18
+ - bootstrap/**
19
+ - client/**
20
+ - diagnostics/**
21
+ - electron/icon.*
20
22
  - electron/tray.*
21
23
  - hub/**
22
24
  - runtime-core/**
package/electron/main.mjs CHANGED
@@ -3,7 +3,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import { createProductUpdateManager } from './update-manager.mjs';
6
+ import { createProductUpdateManager } from './update-manager.mjs';
7
+ import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
7
8
  import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
8
9
  import { getRuntimeAuthEnvironment, resolveDesktopAuthConfig, resolveDesktopBuildFlavor } from './auth-config.mjs';
9
10
  import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
@@ -86,7 +87,13 @@ function appResource(...parts) {
86
87
  return existsSync(unpacked) ? unpacked : resolve(appRoot, ...parts);
87
88
  }
88
89
 
89
- const launcherEntry = appResource('bin', 'livedesk.js');
90
+ // Electron's ASAR loader keeps unpacked files addressable through the virtual
91
+ // app.asar path while resolving their package dependencies from the same tree.
92
+ // Starting from app.asar.unpacked would strand ESM imports such as `ws` and
93
+ // `@supabase/supabase-js` in the sibling archive.
94
+ const launcherEntry = app.isPackaged
95
+ ? resolve(appPath, 'bin', 'livedesk.js')
96
+ : appResource('bin', 'livedesk.js');
90
97
  let mainWindow = null;
91
98
  let tray = null;
92
99
  let runtimeChild = null;
@@ -98,7 +105,8 @@ let runtimeRestartPromise = null;
98
105
  let runtimeLaunchStartedAt = 0;
99
106
  let quitSequenceStarted = false;
100
107
  let updateInstallSequenceStarted = false;
101
- let productUpdates = null;
108
+ let productUpdates = null;
109
+ let recurringProductUpdates = null;
102
110
  let pendingOAuth = null;
103
111
  let pendingProtocolUrl = '';
104
112
  let runtimeLogoutPromise = null;
@@ -390,11 +398,14 @@ function startRuntime(options = {}) {
390
398
  runtimeRestartCount += 1;
391
399
  if (runtimeRestartCount <= 5) {
392
400
  runtimeRestartTimer = setTimeout(() => {
393
- runtimeRestartTimer = null;
394
- if (isQuitting || runtimeChild) return;
395
- startRuntime();
396
- void waitForRuntime().then(ready => { if (ready) mainWindow?.loadURL(LOCAL_RUNTIME_URL); });
397
- }, 1200);
401
+ runtimeRestartTimer = null;
402
+ if (isQuitting || runtimeChild) return;
403
+ startRuntime();
404
+ // Keep the mounted renderer alive while its same-role runtime
405
+ // recovers. Reloading here turns each bounded restart into a white
406
+ // flash and discards useful on-screen state.
407
+ void waitForRuntime().then(ready => { if (ready) updateTray('Running'); });
408
+ }, 1200);
398
409
  } else {
399
410
  updateTray('Runtime stopped');
400
411
  log(`runtime supervisor stopped after ${runtimeRestartCount - 1} retries`);
@@ -530,12 +541,13 @@ function requestRuntimeRestart(source = 'manual') {
530
541
  });
531
542
  }
532
543
 
533
- async function prepareRuntimeForUpdateInstall() {
544
+ async function prepareRuntimeForUpdateInstall() {
534
545
  if (updateInstallSequenceStarted) {
535
546
  throw new Error('desktop-update-install-already-started');
536
547
  }
537
- updateInstallSequenceStarted = true;
538
- isQuitting = true;
548
+ updateInstallSequenceStarted = true;
549
+ isQuitting = true;
550
+ recurringProductUpdates?.stop();
539
551
  try {
540
552
  await stopRuntime();
541
553
  } catch (error) {
@@ -1052,7 +1064,7 @@ if (!app.requestSingleInstanceLock()) {
1052
1064
  app.setAsDefaultProtocolClient('livedesk');
1053
1065
  removeLegacyStartupEntries();
1054
1066
  migrateLegacyPlaintextSession();
1055
- productUpdates = createProductUpdateManager({
1067
+ productUpdates = createProductUpdateManager({
1056
1068
  app,
1057
1069
  onStatus: status => {
1058
1070
  if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
@@ -1063,8 +1075,15 @@ if (!app.requestSingleInstanceLock()) {
1063
1075
  else if (status.state === 'downloaded') updateTray(`Update ${status.availableVersion} ready`);
1064
1076
  },
1065
1077
  beforeInstall: prepareRuntimeForUpdateInstall,
1066
- onInstallError: recoverRuntimeAfterUpdateInstallError
1067
- });
1078
+ onInstallError: recoverRuntimeAfterUpdateInstallError
1079
+ });
1080
+ recurringProductUpdates = createRecurringProductUpdateOwner({
1081
+ manager: productUpdates,
1082
+ canRun: () => !isQuitting && !quitSequenceStarted && !updateInstallSequenceStarted,
1083
+ initialDelayMs: 15_000,
1084
+ intervalMs: 30_000,
1085
+ onError: error => log(`recurring desktop update failed: ${error?.message || error}`)
1086
+ });
1068
1087
  registerIpc();
1069
1088
  powerMonitor.on('resume', () => requestDesktopResumeAuthRecovery('system-resume'));
1070
1089
  powerMonitor.on('unlock-screen', () => requestDesktopResumeAuthRecovery('screen-unlock'));
@@ -1083,8 +1102,9 @@ if (!app.requestSingleInstanceLock()) {
1083
1102
  log(`secure auth session runtime sync failed: ${error?.message || error}`);
1084
1103
  }
1085
1104
  }
1086
- createMainWindow();
1087
- createTray();
1105
+ createMainWindow();
1106
+ createTray();
1107
+ if (DESKTOP_BUILD_FLAVOR !== 'e2e') recurringProductUpdates.start();
1088
1108
  if (DESKTOP_BUILD_FLAVOR === 'e2e') {
1089
1109
  const state = String(process.env.LIVEDESK_E2E_OAUTH_STATE || '');
1090
1110
  const codeVerifier = String(process.env.LIVEDESK_E2E_OAUTH_CODE_VERIFIER || '');
@@ -1097,9 +1117,10 @@ if (!app.requestSingleInstanceLock()) {
1097
1117
  if (initialProtocolUrl) void handleProtocolUrl(initialProtocolUrl);
1098
1118
  });
1099
1119
  app.on('window-all-closed', () => { /* tray-owned desktop stays alive */ });
1100
- app.on('before-quit', event => {
1101
- if (quitSequenceStarted) return;
1102
- event.preventDefault();
1120
+ app.on('before-quit', event => {
1121
+ if (quitSequenceStarted) return;
1122
+ event.preventDefault();
1123
+ recurringProductUpdates?.stop();
1103
1124
  quitSequenceStarted = true;
1104
1125
  isQuitting = true;
1105
1126
  void stopRuntime().then(() => {
@@ -26,14 +26,17 @@ export function createPkceAuthorizationUrl({
26
26
  if (!baseUrl || !publishableKey || !redirectUri || !codeVerifier || !state) {
27
27
  throw new Error('oauth-pkce-parameters-required');
28
28
  }
29
- const url = new URL(`${baseUrl}/auth/v1/authorize`);
30
- url.searchParams.set('provider', 'google');
31
- url.searchParams.set('redirect_to', redirectUri);
32
- url.searchParams.set('code_challenge', createCodeChallenge(codeVerifier));
33
- url.searchParams.set('code_challenge_method', 'S256');
34
- url.searchParams.set('state', state);
35
- url.searchParams.set('scopes', 'email profile');
36
- url.searchParams.set('apikey', publishableKey);
29
+ const url = new URL(`${baseUrl}/auth/v1/authorize`);
30
+ url.searchParams.set('provider', 'google');
31
+ url.searchParams.set('redirect_to', redirectUri);
32
+ url.searchParams.set('code_challenge', createCodeChallenge(codeVerifier));
33
+ url.searchParams.set('code_challenge_method', 'S256');
34
+ // Supabase Auth owns the provider-facing OAuth state for social sign-in.
35
+ // Overriding it here makes Google echo a value Supabase cannot validate and
36
+ // sends the user to the Site URL with `bad_oauth_state`. Keep this nonce only
37
+ // in Electron Main alongside the one-time PKCE verifier.
38
+ url.searchParams.set('scopes', 'email profile');
39
+ url.searchParams.set('apikey', publishableKey);
37
40
  return { url: url.toString(), state, codeVerifier };
38
41
  }
39
42
 
@@ -0,0 +1,98 @@
1
+ const MINIMUM_UPDATE_DELAY_MS = 1_000;
2
+
3
+ function normalizeDelay(value, fallback) {
4
+ const delay = Number(value);
5
+ return Number.isFinite(delay) && delay >= MINIMUM_UPDATE_DELAY_MS
6
+ ? Math.round(delay)
7
+ : fallback;
8
+ }
9
+
10
+ export function createRecurringProductUpdateOwner({
11
+ manager,
12
+ canRun = () => true,
13
+ initialDelayMs = 15_000,
14
+ intervalMs = 30 * 60_000,
15
+ setTimer = setTimeout,
16
+ clearTimer = clearTimeout,
17
+ onError = () => undefined
18
+ } = {}) {
19
+ if (!manager || typeof manager.check !== 'function' || typeof manager.install !== 'function') {
20
+ throw new TypeError('recurring-product-update-manager-required');
21
+ }
22
+
23
+ const initialDelay = normalizeDelay(initialDelayMs, 15_000);
24
+ const interval = normalizeDelay(intervalMs, 30 * 60_000);
25
+ let stopped = true;
26
+ let generation = 0;
27
+ let timer = null;
28
+ let running = null;
29
+
30
+ const clearOwnedTimer = () => {
31
+ if (!timer) return;
32
+ clearTimer(timer);
33
+ timer = null;
34
+ };
35
+
36
+ const arm = (delay, ownerGeneration) => {
37
+ if (stopped || ownerGeneration !== generation || !canRun()) return;
38
+ clearOwnedTimer();
39
+ timer = setTimer(() => {
40
+ if (stopped || ownerGeneration !== generation) return;
41
+ timer = null;
42
+ void runCycle(ownerGeneration);
43
+ }, delay);
44
+ timer?.unref?.();
45
+ };
46
+
47
+ const runCycle = ownerGeneration => {
48
+ if (stopped || ownerGeneration !== generation || !canRun()) {
49
+ return Promise.resolve(manager.getStatus?.() || { state: 'stopped' });
50
+ }
51
+ if (running) return running;
52
+
53
+ const operation = (async () => {
54
+ const checked = await manager.check();
55
+ if (!stopped
56
+ && ownerGeneration === generation
57
+ && canRun()
58
+ && checked?.downloaded === true) {
59
+ return manager.install();
60
+ }
61
+ return checked;
62
+ })();
63
+ running = operation;
64
+ void operation.catch(error => onError(error)).finally(() => {
65
+ if (running === operation) running = null;
66
+ arm(interval, ownerGeneration);
67
+ });
68
+ return operation;
69
+ };
70
+
71
+ return {
72
+ start() {
73
+ if (!stopped) return;
74
+ stopped = false;
75
+ generation += 1;
76
+ arm(initialDelay, generation);
77
+ },
78
+ stop() {
79
+ stopped = true;
80
+ generation += 1;
81
+ clearOwnedTimer();
82
+ },
83
+ runNow() {
84
+ clearOwnedTimer();
85
+ return runCycle(generation);
86
+ },
87
+ inspect() {
88
+ return {
89
+ stopped,
90
+ generation,
91
+ timerCount: timer ? 1 : 0,
92
+ running: Boolean(running),
93
+ initialDelayMs: initialDelay,
94
+ intervalMs: interval
95
+ };
96
+ }
97
+ };
98
+ }
@@ -11,21 +11,30 @@ export function createProductUpdateManager({
11
11
  beforeInstall = async () => undefined,
12
12
  onInstallError = async () => undefined,
13
13
  updater = null,
14
- platform = process.platform,
15
- arch = process.arch,
16
- startupCheckTimeoutMs = 2_500,
17
- updateFeedConfig = null
18
- } = {}) {
19
- if (!updater) updater = require('electron-updater').autoUpdater;
20
- let feedConfig = updateFeedConfig;
21
- if (!feedConfig) {
22
- try {
23
- feedConfig = JSON.parse(readFileSync(join(app.getAppPath(), 'electron', 'update-feed.json'), 'utf8'));
14
+ platform = process.platform,
15
+ arch = process.arch,
16
+ startupCheckTimeoutMs = 2_500,
17
+ updateFeedConfig = null,
18
+ updateChannel = ''
19
+ } = {}) {
20
+ if (!updater) updater = require('electron-updater').autoUpdater;
21
+ let feedConfig = updateFeedConfig;
22
+ let packageMetadata = {};
23
+ try {
24
+ packageMetadata = JSON.parse(readFileSync(join(app.getAppPath(), 'package.json'), 'utf8'));
25
+ } catch {
26
+ packageMetadata = {};
27
+ }
28
+ if (!feedConfig) {
29
+ try {
30
+ feedConfig = JSON.parse(readFileSync(join(app.getAppPath(), 'electron', 'update-feed.json'), 'utf8'));
24
31
  } catch {
25
- feedConfig = {};
26
- }
27
- }
28
- const feed = resolveDesktopUpdateFeed({
32
+ feedConfig = {};
33
+ }
34
+ }
35
+ const packagedChannel = String(updateChannel || packageMetadata?.desktopUpdateChannel || '').trim().toLowerCase();
36
+ if (packagedChannel) feedConfig = { ...feedConfig, channel: packagedChannel };
37
+ const feed = resolveDesktopUpdateFeed({
29
38
  config: feedConfig,
30
39
  overrideUrl: process.env.LIVEDESK_UPDATE_URL,
31
40
  platform,
@@ -46,8 +55,9 @@ export function createProductUpdateManager({
46
55
  startupCheck: 'idle'
47
56
  };
48
57
  let installPromise = null;
49
- let checkPromise = null;
50
- let downloadPromise = null;
58
+ let checkPromise = null;
59
+ let downloadPromise = null;
60
+ let cyclePromise = null;
51
61
  let startupPromise = null;
52
62
  let checkOutcome = 'idle';
53
63
  let installRuntimePrepared = false;
@@ -170,11 +180,20 @@ export function createProductUpdateManager({
170
180
  }
171
181
  };
172
182
 
173
- const checkAndDownload = async () => {
174
- const checked = await checkForUpdates();
175
- if (checked.state === 'available') return downloadUpdate();
176
- return checked;
177
- };
183
+ const checkAndDownload = async () => {
184
+ if (cyclePromise) return cyclePromise;
185
+ const operation = (async () => {
186
+ const checked = await checkForUpdates();
187
+ if (checked.state === 'available') return downloadUpdate();
188
+ return checked;
189
+ })();
190
+ cyclePromise = operation;
191
+ try {
192
+ return await operation;
193
+ } finally {
194
+ if (cyclePromise === operation) cyclePromise = null;
195
+ }
196
+ };
178
197
 
179
198
  return {
180
199
  getStatus() {
@@ -227,7 +246,9 @@ export function createProductUpdateManager({
227
246
  await beforeInstall();
228
247
  installRuntimePrepared = true;
229
248
  publish({ state: 'installing', error: '' });
230
- updater.quitAndInstall(false, true);
249
+ // Long-running desktop Clients must not stop at the assisted NSIS
250
+ // wizard after the runtime has already shut down for replacement.
251
+ updater.quitAndInstall(true, true);
231
252
  return snapshot();
232
253
  } catch (error) {
233
254
  await recoverPreparedRuntime(error);
@@ -770,7 +770,7 @@ function buildRemoteFrameTransferDescriptorFromMessage(message = {}, fallback =
770
770
  };
771
771
  }
772
772
 
773
- function normalizeIncomingFrameModeProfile(value) {
773
+ function normalizeIncomingFrameModeProfile(value) {
774
774
  if (!value) {
775
775
  return null;
776
776
  }
@@ -794,8 +794,27 @@ function normalizeIncomingFrameModeProfile(value) {
794
794
  profile: safeString(value.profile || value.frameProfile, 80) || profile.frameProfile,
795
795
  modeVersion: Number.isFinite(Number(value.modeVersion)) ? Math.max(0, Math.floor(Number(value.modeVersion))) : 1,
796
796
  implemented: value.implemented !== false
797
- };
798
- }
797
+ };
798
+ }
799
+
800
+ function deviceExplicitlyRejectsFrameMode(device, frameMode) {
801
+ const requestedMode = safeString(frameMode, 80).toLowerCase();
802
+ if (!requestedMode) {
803
+ return false;
804
+ }
805
+
806
+ const advertisedModes = [
807
+ ...(Array.isArray(device?.frameModes) ? device.frameModes : []),
808
+ ...(Array.isArray(device?.frameProtocol?.modes) ? device.frameProtocol.modes : []),
809
+ ...(Array.isArray(device?.capabilities?.frameModes) ? device.capabilities.frameModes : []),
810
+ ...(Array.isArray(device?.capabilities?.frameProtocol?.modes)
811
+ ? device.capabilities.frameProtocol.modes
812
+ : [])
813
+ ];
814
+ return advertisedModes.some(profile =>
815
+ safeString(profile?.mode || profile?.frameMode, 80).toLowerCase() === requestedMode
816
+ && profile?.implemented === false);
817
+ }
799
818
 
800
819
  function isLoopbackHost(value) {
801
820
  const host = String(value || '').trim().toLowerCase();
@@ -9660,11 +9679,18 @@ export function createRemoteHub(options = {}) {
9660
9679
  }
9661
9680
 
9662
9681
  const now = new Date().toISOString();
9663
- const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
9664
- const modePolicyError = liveStreamModePolicyError(normalized);
9665
- if (modePolicyError) return { ok: false, error: modePolicyError };
9666
- const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
9667
- const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
9682
+ const normalized = normalizeLiveStreamStartOptions({ ...options, platform: device.platform });
9683
+ const modePolicyError = liveStreamModePolicyError(normalized);
9684
+ if (modePolicyError) return { ok: false, error: modePolicyError };
9685
+ const { fps, maxWidth, maxHeight, quality, monitorIndex, transfer, streamPurpose } = normalized;
9686
+ if (deviceExplicitlyRejectsFrameMode(device, transfer.frameMode)) {
9687
+ return {
9688
+ ok: false,
9689
+ error: 'device-frame-mode-unavailable',
9690
+ frameMode: transfer.frameMode
9691
+ };
9692
+ }
9693
+ const streamId = safeString(options.streamId, 128) || makeStableLiveStreamId(deviceId, streamPurpose);
9668
9694
  if (!reserveDeviceLiveStreamDescriptor(device, streamId)) {
9669
9695
  return {
9670
9696
  ok: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.554",
4
- "livedeskClientVersion": "0.1.248",
3
+ "version": "0.1.563",
4
+ "livedeskClientVersion": "0.1.248",
5
5
  "buildFlavor": "production",
6
6
  "description": "LiveDesk Hub and client launcher",
7
7
  "type": "module",
@@ -40,22 +40,21 @@
40
40
  "node": ">=20"
41
41
  },
42
42
  "dependencies": {
43
- "@ffmpeg-installer/ffmpeg": "^1.1.0",
44
- "ffmpeg-static": "^5.3.0",
45
43
  "@openai/codex-sdk": "0.145.0",
46
44
  "@supabase/supabase-js": "^2.110.0",
47
45
  "cors": "^2.8.5",
48
46
  "electron-updater": "^6.8.9",
49
47
  "express": "^4.21.2",
48
+ "ffmpeg-static": "^5.3.0",
50
49
  "node-screenshots": "^0.2.8",
51
50
  "path-to-regexp": "0.1.13",
52
51
  "ws": "^8.18.3"
53
52
  },
54
53
  "optionalDependencies": {
55
- "@livedesk/fast-linux-x64": "0.1.445",
56
- "@livedesk/fast-osx-arm64": "0.1.445",
57
- "@livedesk/fast-osx-x64": "0.1.445",
58
- "@livedesk/fast-win-x64": "0.1.445"
54
+ "@livedesk/fast-linux-x64": "0.1.445",
55
+ "@livedesk/fast-osx-arm64": "0.1.445",
56
+ "@livedesk/fast-osx-x64": "0.1.445",
57
+ "@livedesk/fast-win-x64": "0.1.445"
59
58
  },
60
59
  "publishConfig": {
61
60
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  export const PRODUCTION_SUPABASE_URL = 'https://otbyfkjxrkngvjziawki.supabase.co';
2
2
  export const PRODUCTION_SUPABASE_PUBLISHABLE_KEY = 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
3
- export const PRODUCTION_OAUTH_REDIRECT_URI = 'livedesk://auth/callback';
3
+ export const PRODUCTION_OAUTH_REDIRECT_URI = 'https://livedesk.pages.dev/desktop-auth-callback';
4
4
  export const PRODUCTION_BUILD_FLAVOR = 'production';
5
5
  export const E2E_BUILD_FLAVOR = 'e2e';
6
6