livedesk 0.1.697 → 0.1.701

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.
Files changed (42) hide show
  1. package/README.md +21 -3
  2. package/bin/livedesk.js +16 -8
  3. package/client/package.json +5 -5
  4. package/diagnostics/vuvodesk-linux-desktop.mjs +188 -0
  5. package/electron/entitlements.mac.plist +12 -12
  6. package/electron/recurring-update-owner.mjs +2 -2
  7. package/electron/update-manager.mjs +59 -16
  8. package/hub/package.json +1 -1
  9. package/hub/src/plan-device-access.test.mjs +8 -8
  10. package/hub/src/remote-hub.js +12 -3
  11. package/hub/src/server.js +1 -1
  12. package/package.json +7 -7
  13. package/web/dist/app.html +1 -1
  14. package/web/dist/app.webmanifest +1 -1
  15. package/web/dist/assets/{AgentSettingsTab-DRWDCG0o.js → AgentSettingsTab-D0oebRk-.js} +1 -1
  16. package/web/dist/assets/{AgentsPage-BnF3p9G1.js → AgentsPage-BBj64SpA.js} +1 -1
  17. package/web/dist/assets/{CaptureGallery-8lr9Ct1i.js → CaptureGallery-DF6gZHy0.js} +1 -1
  18. package/web/dist/assets/DesktopUpdatePanel-BaRZzRao.js +1 -0
  19. package/web/dist/assets/HubApp-C-E5cpCr.css +1 -0
  20. package/web/dist/assets/HubApp-DJsiVqWP.js +182 -0
  21. package/web/dist/assets/{MonitorStackIcon-led0pnag.js → MonitorStackIcon-BD7Yn-Z9.js} +5 -5
  22. package/web/dist/assets/{SettingsPage-k202bRU1.js → SettingsPage-CXmk161K.js} +1 -1
  23. package/web/dist/assets/SettingsTabs-C_xNvq7v.js +1 -0
  24. package/web/dist/assets/{ShareFilesPage-DRW1qS1Z.js → ShareFilesPage-Dx5yYsbk.js} +1 -1
  25. package/web/dist/assets/{SupportPage-DXkBQh_W.js → SupportPage-DAsms8Kr.js} +1 -1
  26. package/web/dist/assets/{VuvoDeskApp-BHcF72r3.js → VuvoDeskApp-9sUnuvyS.js} +1 -1
  27. package/web/dist/assets/{app-DcrmhmnT.js → app-CUM2wTSb.js} +2 -2
  28. package/web/dist/assets/{client-D-D35ghV.js → client-DV0v4wud.js} +1 -1
  29. package/web/dist/assets/frame-lifecycle-D-BxeTQV.js +1 -0
  30. package/web/dist/assets/{icons-ChMazCZR.js → icons-BwhnWKHR.js} +1 -1
  31. package/web/dist/assets/main-BbK4NG31.js +2 -0
  32. package/web/dist/assets/{pwa-startup-BiuYh5eN.js → pwa-startup-C8kDd3Vd.js} +1 -1
  33. package/web/dist/assets/{react-CnkEM7Rb.js → react-Dm67yaJf.js} +1 -1
  34. package/web/dist/index.html +6 -6
  35. package/web/dist/sw.js +2 -2
  36. package/web/dist/vuvodesk-build-evidence.json +58 -58
  37. package/web/dist/assets/DesktopUpdatePanel-DopP2EeD.js +0 -1
  38. package/web/dist/assets/HubApp-CqNxUVJD.js +0 -182
  39. package/web/dist/assets/HubApp-DdHHVOM0.css +0 -1
  40. package/web/dist/assets/SettingsTabs-v57M-CCZ.js +0 -1
  41. package/web/dist/assets/frame-lifecycle-D91nchvm.js +0 -1
  42. package/web/dist/assets/main-BXBJt96-.js +0 -2
package/README.md CHANGED
@@ -44,7 +44,25 @@ npx -y --prefer-online vuvodesk@latest --force-role hub
44
44
  npx -y --prefer-online vuvodesk@latest --force-role client
45
45
  ```
46
46
 
47
- ## Hub
47
+ ## Linux desktop diagnosis
48
+
49
+ The source launcher provides a read-only preparation diagnostic:
50
+
51
+ ```sh
52
+ node packages/vuvodesk/bin/livedesk.js diagnose linux-desktop
53
+ node packages/vuvodesk/bin/livedesk.js diagnose linux-desktop --pid <native-agent-pid>
54
+ ```
55
+
56
+ Run from a graphical terminal, or specify the existing same-user
57
+ `livedesk-client-fast` PID when using SSH. It reports Xorg/Wayland session
58
+ evidence and the system's ScreenCast/RemoteDesktop portal properties. It does
59
+ not start capture, send input, show permission prompts, restart the runtime or
60
+ change OS settings. Portal presence does not mean VuvoDesk Wayland support is
61
+ implemented. The current capture/input path is X11; Wayland integration remains
62
+ in preparation. This command must be included in a future published launcher
63
+ before it is available through `vuvodesk@latest`.
64
+
65
+ ## Hub
48
66
 
49
67
  ```powershell
50
68
  npx -y --prefer-online vuvodesk@latest hub
@@ -154,8 +172,8 @@ device, Hub URL, timeout, and report-path options.
154
172
 
155
173
  ## Plans
156
174
 
157
- - Free: 5 personal devices with a standard wall banner ad.
158
- - Plus LTD launch: 15 personal devices, no ads, USD 79 one-time.
175
+ - Free: 3 personal devices with a standard wall banner ad.
176
+ - Plus LTD launch: 20 devices for personal and commercial use, no ads, USD 79 one-time.
159
177
  - Pro LTD launch: 50 devices for schools, academies, and commercial use, USD 199 one-time.
160
178
  - Team: unlimited devices, up to 5 people, and commercial use.
161
179
 
package/bin/livedesk.js CHANGED
@@ -347,7 +347,8 @@ Usage:
347
347
  npx -y --prefer-online vuvodesk@latest --force-role hub
348
348
  npx -y --prefer-online vuvodesk@latest --force-role client
349
349
  npx -y --prefer-online vuvodesk@latest diagnose mac-physical --confirm-active-motion
350
- npx -y --prefer-online vuvodesk@latest diagnose transport-physical
350
+ npx -y --prefer-online vuvodesk@latest diagnose transport-physical
351
+ npx -y --prefer-online vuvodesk@latest diagnose linux-desktop
351
352
 
352
353
  Commands:
353
354
  No command Resolve the signed-in device role and start the matching runtime.
@@ -355,8 +356,10 @@ Commands:
355
356
  client Legacy alias for --force-role client.
356
357
  diagnose mac-physical
357
358
  Run the isolated two-display macOS capture/control physical gate.
358
- diagnose transport-physical
359
- Prove Direct, UDP P2P, encrypted relay, and Direct recovery on a current stream.
359
+ diagnose transport-physical
360
+ Prove Direct, UDP P2P, encrypted relay, and Direct recovery on a current stream.
361
+ diagnose linux-desktop
362
+ Inspect Linux session and Wayland portal availability without starting capture.
360
363
 
361
364
  Hub options:
362
365
  --no-open Do not open the browser automatically.
@@ -419,7 +422,7 @@ async function runDiagnosticCommand(args) {
419
422
  printHelp();
420
423
  return;
421
424
  }
422
- if (name === 'transport-physical') {
425
+ if (name === 'transport-physical') {
423
426
  const diagnosticEntry = resolve(
424
427
  packageRoot,
425
428
  'diagnostics',
@@ -430,10 +433,15 @@ async function runDiagnosticCommand(args) {
430
433
  }
431
434
  const diagnostic = await import(pathToFileURL(diagnosticEntry).href);
432
435
  await diagnostic.runTransportPhysicalDiagnosticCli(args.slice(1));
433
- return;
434
- }
435
- if (name !== 'mac-physical') {
436
- throw new Error(`Unknown VuvoDesk diagnostic: ${name}. Supported diagnostics: mac-physical, transport-physical.`);
436
+ return;
437
+ }
438
+ if (name === 'linux-desktop') {
439
+ const diagnostic = await import('../diagnostics/vuvodesk-linux-desktop.mjs');
440
+ await diagnostic.runLinuxDesktopDiagnosticCli(args.slice(1));
441
+ return;
442
+ }
443
+ if (name !== 'mac-physical') {
444
+ throw new Error(`Unknown VuvoDesk diagnostic: ${name}. Supported diagnostics: mac-physical, transport-physical, linux-desktop.`);
437
445
  }
438
446
  if (args.slice(1).some(argument => ['--help', '-h', 'help'].includes(String(argument).toLowerCase()))) {
439
447
  printMacPhysicalDiagnosticHelp();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.276",
3
+ "version": "0.1.277",
4
4
  "description": "VuvoDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,10 +42,10 @@
42
42
  "ws": "^8.18.3"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@livedesk/fast-linux-x64": "0.1.471",
46
- "@livedesk/fast-osx-arm64": "0.1.471",
47
- "@livedesk/fast-osx-x64": "0.1.471",
48
- "@livedesk/fast-win-x64": "0.1.471"
45
+ "@livedesk/fast-linux-x64": "0.1.472",
46
+ "@livedesk/fast-osx-arm64": "0.1.472",
47
+ "@livedesk/fast-osx-x64": "0.1.472",
48
+ "@livedesk/fast-win-x64": "0.1.472"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from 'node:child_process';
3
+ import { open, readlink, stat } from 'node:fs/promises';
4
+ import { basename } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+
8
+ const exec = promisify(execFile);
9
+ const ENV_KEYS = ['DISPLAY', 'WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'XDG_CURRENT_DESKTOP',
10
+ 'XDG_SESSION_DESKTOP', 'XDG_RUNTIME_DIR', 'DBUS_SESSION_BUS_ADDRESS'];
11
+ const PORTAL_LIMIT = 16384;
12
+
13
+ function sessionEnvironment(env) {
14
+ return Object.fromEntries(ENV_KEYS.filter(key => typeof env[key] === 'string'
15
+ && env[key].length <= 4096).map(key => [key, env[key]]));
16
+ }
17
+
18
+ async function readBoundedText(path, limit = 8192) {
19
+ const file = await open(path, 'r');
20
+ try {
21
+ const data = Buffer.alloc(limit + 1);
22
+ const { bytesRead } = await file.read(data, 0, data.length, 0);
23
+ if (bytesRead > limit) throw new Error('linux-desktop-file-limit');
24
+ return data.toString('utf8', 0, bytesRead);
25
+ } finally { await file.close(); }
26
+ }
27
+
28
+ export function classifyLinuxDesktopSession(env = {}) {
29
+ const explicit = String(env.XDG_SESSION_TYPE || '').trim().toLowerCase();
30
+ const type = explicit === 'x11' || explicit === 'wayland' ? explicit
31
+ : env.WAYLAND_DISPLAY ? 'wayland'
32
+ : env.DISPLAY ? 'x11' : explicit === 'tty' ? 'headless' : 'unknown';
33
+ const source = explicit === 'x11' || explicit === 'wayland' ? 'xdg-session-type'
34
+ : env.WAYLAND_DISPLAY ? 'wayland-display'
35
+ : env.DISPLAY ? 'display-only' : 'no-graphical-evidence';
36
+ const rawDesktop = String(env.XDG_CURRENT_DESKTOP || env.XDG_SESSION_DESKTOP || '').toLowerCase();
37
+ const desktop = ['gnome', 'kde', 'lxqt', 'xfce', 'cinnamon', 'sway', 'hyprland']
38
+ .find(name => rawDesktop.split(/[:;\s]+/).includes(name)) || 'unknown';
39
+ return { type, source, desktop, xDisplayPresent: Boolean(env.DISPLAY),
40
+ waylandDisplayPresent: Boolean(env.WAYLAND_DISPLAY) };
41
+ }
42
+
43
+ function processStartTime(text) {
44
+ const fields = text.slice(text.lastIndexOf(')') + 2).trim().split(/\s+/);
45
+ if (!/^\d+$/.test(fields[19] || '')) throw new Error('linux-desktop-invalid-process-stat');
46
+ return fields[19];
47
+ }
48
+
49
+ export async function readNativeAgentContext(pid, {
50
+ uid = process.getuid?.(), readText = readBoundedText, readLink = readlink, statPath = stat
51
+ } = {}) {
52
+ if (!Number.isSafeInteger(pid) || pid < 2) throw new Error('linux-desktop-invalid-pid');
53
+ const root = `/proc/${pid}`;
54
+ const first = processStartTime(await readText(`${root}/stat`, 4096));
55
+ if ((await statPath(root)).uid !== uid) throw new Error('linux-desktop-requires-same-user-agent');
56
+ const executable = await readLink(`${root}/exe`);
57
+ if (basename(executable) !== 'livedesk-client-fast') throw new Error('linux-desktop-requires-native-agent');
58
+ const raw = await readText(`${root}/environ`, 131072);
59
+ const all = Object.fromEntries(raw.split('\0').filter(x => x.includes('=')).map(x => {
60
+ const equals = x.indexOf('=');
61
+ return [x.slice(0, equals), x.slice(equals + 1)];
62
+ }));
63
+ const env = sessionEnvironment(all);
64
+ const assertCurrent = async () => {
65
+ if (processStartTime(await readText(`${root}/stat`, 4096)) !== first
66
+ || await readLink(`${root}/exe`) !== executable) throw new Error('linux-desktop-agent-changed');
67
+ };
68
+ await assertCurrent();
69
+ return { env, binding: { pid, startTime: first }, assertCurrent };
70
+ }
71
+
72
+ export function parsePortalProperties(text, kind) {
73
+ if (typeof text !== 'string' || text.length > PORTAL_LIMIT) throw new Error('portal-properties-invalid');
74
+ const integer = key => {
75
+ const match = text.match(new RegExp(`['"]${key}['"]\\s*:\\s*<uint32\\s+(\\d+)>`));
76
+ if (!match) return null;
77
+ const value = Number(match[1]);
78
+ return Number.isSafeInteger(value) && value >= 0 && value <= 0xffffffff ? value : null;
79
+ };
80
+ const version = integer('version');
81
+ const sourceTypes = kind === 'ScreenCast' ? integer('AvailableSourceTypes') : null;
82
+ const deviceTypes = kind === 'RemoteDesktop' ? integer('AvailableDeviceTypes') : null;
83
+ if (!version || (kind === 'ScreenCast' ? sourceTypes === null : deviceTypes === null)) {
84
+ throw new Error('portal-properties-invalid');
85
+ }
86
+ return { state: 'available', version, sourceTypes, deviceTypes,
87
+ cursorModes: kind === 'ScreenCast' ? integer('AvailableCursorModes') : null };
88
+ }
89
+
90
+ function missingPortal(state) {
91
+ return { state, version: null, sourceTypes: null, deviceTypes: null, cursorModes: null };
92
+ }
93
+
94
+ async function inspectPortal(kind, env, run) {
95
+ if (!env.DBUS_SESSION_BUS_ADDRESS || !env.DBUS_SESSION_BUS_ADDRESS.startsWith('unix:')) {
96
+ return missingPortal('session-bus-unavailable');
97
+ }
98
+ try {
99
+ const result = await run('/usr/bin/gdbus', ['call', '--session', '--dest',
100
+ 'org.freedesktop.portal.Desktop', '--object-path', '/org/freedesktop/portal/desktop',
101
+ '--method', 'org.freedesktop.DBus.Properties.GetAll', `org.freedesktop.portal.${kind}`], {
102
+ env: { ...sessionEnvironment(env), PATH: '/usr/bin:/bin', LANG: 'C.UTF-8' },
103
+ encoding: 'utf8', timeout: 1500, maxBuffer: PORTAL_LIMIT,
104
+ killSignal: 'SIGKILL', shell: false, windowsHide: true
105
+ });
106
+ return parsePortalProperties(result.stdout, kind);
107
+ } catch (error) {
108
+ const state = error?.code === 'ENOENT' ? 'probe-tool-unavailable'
109
+ : error?.killed || error?.code === 'ETIMEDOUT' ? 'timeout'
110
+ : error?.message === 'portal-properties-invalid' ? 'properties-unavailable'
111
+ : 'query-failed';
112
+ return missingPortal(state);
113
+ }
114
+ }
115
+
116
+ function distribution(text) {
117
+ const field = key => {
118
+ const value = String(text).match(new RegExp(`^${key}=(.*)$`, 'm'))?.[1]?.trim().replace(/^"|"$/g, '');
119
+ return value && /^[a-zA-Z0-9._ -]{1,80}$/.test(value) ? value : null;
120
+ };
121
+ return { id: field('ID'), versionId: field('VERSION_ID') };
122
+ }
123
+
124
+ export async function inspectLinuxDesktop({ platform = process.platform, env = process.env,
125
+ readText = readBoundedText, run = exec, assertCurrent = async () => {}, binding = null
126
+ } = {}) {
127
+ if (platform !== 'linux') return { schemaVersion: 1, state: 'not-linux' };
128
+ // Snapshot only session fields. Never emit addresses, paths, environment dumps,
129
+ // portal request/restore tokens or raw command errors.
130
+ const context = sessionEnvironment(env);
131
+ const session = classifyLinuxDesktopSession(context);
132
+ let os = { id: null, versionId: null };
133
+ try { os = distribution(await readText('/etc/os-release', 8192)); } catch { /* unknown */ }
134
+ await assertCurrent();
135
+ const screenCast = await inspectPortal('ScreenCast', context, run);
136
+ await assertCurrent();
137
+ const remoteDesktop = await inspectPortal('RemoteDesktop', context, run);
138
+ await assertCurrent();
139
+ return {
140
+ schemaVersion: 1, state: 'inspected', distribution: os, session, binding,
141
+ portals: { screenCast, remoteDesktop,
142
+ monitorAdvertised: screenCast.sourceTypes === null ? null : (screenCast.sourceTypes & 1) !== 0,
143
+ keyboardPointerAdvertised: remoteDesktop.deviceTypes === null ? null : (remoteDesktop.deviceTypes & 3) === 3 },
144
+ vuvodesk: {
145
+ requiredBackend: session.type === 'wayland' ? 'wayland-portal' : session.type === 'x11' ? 'x11' : 'unknown',
146
+ waylandBackendImplemented: false, permissionGranted: null, captureTested: false, inputTested: false,
147
+ note: session.type === 'wayland'
148
+ ? 'Wayland capture and input are not implemented yet. Portal availability is not permission or working video.'
149
+ : 'Read-only environment inspection. Capture, input and performance are not tested.'
150
+ }
151
+ };
152
+ }
153
+
154
+ export function parseLinuxDesktopArgs(args) {
155
+ let pid = null;
156
+ let help = false;
157
+ for (let index = 0; index < args.length; index += 1) {
158
+ const arg = args[index];
159
+ if (arg === '--help' || arg === '-h') help = true;
160
+ else if (arg === '--pid' && pid === null && /^[1-9]\d*$/.test(args[index + 1] || '')) {
161
+ pid = Number(args[++index]);
162
+ if (!Number.isSafeInteger(pid) || pid < 2) throw new Error('linux-desktop-invalid-pid');
163
+ } else throw new Error('linux-desktop-invalid-arguments');
164
+ }
165
+ return { pid, help };
166
+ }
167
+
168
+ export async function runLinuxDesktopDiagnosticCli(args = []) {
169
+ const { pid, help } = parseLinuxDesktopArgs(args);
170
+ if (help) {
171
+ console.log(`VuvoDesk Linux desktop / Wayland preparation diagnostic
172
+ Usage: vuvodesk diagnose linux-desktop [--pid <native-agent-pid>]
173
+ Run from the graphical terminal, or use --pid over SSH for an existing same-user
174
+ livedesk-client-fast process. Reads only session and portal properties as JSON.
175
+ No capture, input, approval prompt, OS setting change, runtime restart or files.
176
+ Portal availability does not establish working Wayland support or permission.`);
177
+ return;
178
+ }
179
+ const context = pid && process.platform === 'linux' ? await readNativeAgentContext(pid) : {};
180
+ console.log(JSON.stringify(await inspectLinuxDesktop(context), null, 2));
181
+ }
182
+
183
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
184
+ runLinuxDesktopDiagnosticCli(process.argv.slice(2)).catch(() => {
185
+ console.error('Linux desktop diagnosis failed. Check arguments and the current same-user native Agent PID.');
186
+ process.exitCode = 1;
187
+ });
188
+ }
@@ -1,12 +1,12 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>com.apple.security.cs.allow-jit</key>
6
- <true/>
7
- <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
- <true/>
9
- <key>com.apple.security.device.audio-input</key>
10
- <true/>
11
- </dict>
12
- </plist>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ <key>com.apple.security.device.audio-input</key>
10
+ <true/>
11
+ </dict>
12
+ </plist>
@@ -103,12 +103,12 @@ export function createRecurringProductUpdateOwner(options = {}) {
103
103
  if (running) return running;
104
104
 
105
105
  const operation = (async () => {
106
- const checked = await manager.check();
106
+ const checked = await manager.check({ automatic: true });
107
107
  if (!stopped
108
108
  && ownerGeneration === generation
109
109
  && canRun()
110
110
  && checked?.downloaded === true) {
111
- return manager.install();
111
+ return manager.install({ automatic: true });
112
112
  }
113
113
  return checked;
114
114
  })();
@@ -130,6 +130,8 @@ export function createProductUpdateManager({
130
130
  let activeOwner = null;
131
131
  let availableDownload = null;
132
132
  let installRuntimePrepared = false;
133
+ let installTargetVersion = '';
134
+ let automaticInstallFailure = null;
133
135
  let installRecoveryPromise = null;
134
136
  const checkDeadline = normalizeDeadline(checkDeadlineMs, DEFAULT_CHECK_DEADLINE_MS);
135
137
  const downloadStallTimeout = normalizeDeadline(downloadStallTimeoutMs, DEFAULT_DOWNLOAD_STALL_TIMEOUT_MS);
@@ -154,6 +156,27 @@ export function createProductUpdateManager({
154
156
  onStatus({ ...status });
155
157
  };
156
158
  const snapshot = () => ({ ...status, packaged: Boolean(app.isPackaged) });
159
+ const rememberInstallerFailure = error => {
160
+ const message = String(error?.message || error);
161
+ const failure = {
162
+ state: 'error',
163
+ error: message,
164
+ errorCode: platform === 'linux' && /Command pkexec exited with code 127\b/.test(message)
165
+ ? 'desktop-update-authorization-required'
166
+ : 'desktop-update-installer-launch-failed'
167
+ };
168
+ if (installTargetVersion) {
169
+ automaticInstallFailure = { version: installTargetVersion, failure };
170
+ updater.autoInstallOnAppQuit = false;
171
+ }
172
+ return failure;
173
+ };
174
+ const blockedAutomaticInstall = () => {
175
+ if (!automaticInstallFailure
176
+ || automaticInstallFailure.version !== status.availableVersion) return null;
177
+ publish(automaticInstallFailure.failure);
178
+ return snapshot();
179
+ };
157
180
  const releaseAvailableDownload = record => {
158
181
  const owned = record || availableDownload;
159
182
  if (!owned) return;
@@ -417,7 +440,7 @@ export function createProductUpdateManager({
417
440
  const connectUpdater = candidate => {
418
441
  if (!candidate || connectedUpdaters.has(candidate)) return;
419
442
  candidate.autoDownload = false;
420
- candidate.autoInstallOnAppQuit = true;
443
+ candidate.autoInstallOnAppQuit = !automaticInstallFailure;
421
444
  if (feed.url) {
422
445
  // Enable multipart reads only for the exact supported electron-updater
423
446
  // implementation patched with VuvoDesk's 48-range/32 MiB batch owner.
@@ -521,11 +544,7 @@ export function createProductUpdateManager({
521
544
  const owner = activeOwner?.acceptsEvents === true ? activeOwner : null;
522
545
  if (!owner) {
523
546
  if (!installRuntimePrepared) return;
524
- publish({
525
- state: 'error',
526
- error: String(error?.message || error),
527
- errorCode: 'desktop-update-installer-launch-failed'
528
- });
547
+ publish(rememberInstallerFailure(error));
529
548
  void recoverRuntime();
530
549
  return;
531
550
  }
@@ -634,6 +653,7 @@ export function createProductUpdateManager({
634
653
  });
635
654
  }
636
655
  if (owner.outcome === 'available') {
656
+ updater.autoInstallOnAppQuit = automaticInstallFailure?.version !== owner.expectedVersion;
637
657
  availableDownload = {
638
658
  generation: owner.generation,
639
659
  version: String(owner.expectedVersion || result?.updateInfo?.version || status.availableVersion || ''),
@@ -791,10 +811,17 @@ export function createProductUpdateManager({
791
811
  }
792
812
  };
793
813
 
794
- const checkAndDownload = async () => {
814
+ const checkAndDownload = async ({ automatic = false } = {}) => {
795
815
  if (cyclePromise) return cyclePromise;
796
816
  const operation = (async () => {
797
- const checked = await checkForUpdates();
817
+ const checked = await checkForUpdates();
818
+ // Continue discovering newer versions, but a failed installer must not
819
+ // repeatedly stop a healthy Client or ask for authorization on each poll.
820
+ if (automatic && !checked.reopenRequired
821
+ && (checked.state === 'available' || checked.state === 'downloaded')) {
822
+ const blocked = blockedAutomaticInstall();
823
+ if (blocked) return blocked;
824
+ }
798
825
  if (checked.state === 'available') return downloadUpdate();
799
826
  return checked;
800
827
  })();
@@ -810,15 +837,15 @@ export function createProductUpdateManager({
810
837
  getStatus() {
811
838
  return snapshot();
812
839
  },
813
- async check() {
814
- return checkAndDownload();
840
+ async check(options) {
841
+ return checkAndDownload(options);
815
842
  },
816
843
  async checkAtStartup() {
817
844
  if (startupPromise) return startupPromise;
818
845
  const operation = (async () => {
819
846
  if (!app.isPackaged || !feed.url) return checkForUpdates();
820
847
  publish({ startupCheck: 'checking' });
821
- const background = checkAndDownload().finally(() => {
848
+ const background = checkAndDownload({ automatic: true }).finally(() => {
822
849
  publish({ startupCheck: 'complete' });
823
850
  });
824
851
  let timeoutId;
@@ -841,7 +868,7 @@ export function createProductUpdateManager({
841
868
  if (startupPromise === operation) startupPromise = null;
842
869
  }
843
870
  },
844
- async install() {
871
+ async install({ automatic = false } = {}) {
845
872
  if (!app.isPackaged) return { ...snapshot(), state: 'development', error: 'desktop-updates-require-packaged-app' };
846
873
  if (!feed.url) return unavailableStatus();
847
874
  if (status.reopenRequired) return snapshot();
@@ -850,6 +877,10 @@ export function createProductUpdateManager({
850
877
  return snapshot();
851
878
  }
852
879
  if (installPromise) return installPromise;
880
+ if (automatic) {
881
+ const blocked = blockedAutomaticInstall();
882
+ if (blocked) return blocked;
883
+ }
853
884
  installOperationPending = true;
854
885
  const operation = (async () => {
855
886
  try {
@@ -867,9 +898,20 @@ export function createProductUpdateManager({
867
898
  if (!status.downloaded) {
868
899
  const downloadResult = availableDownload
869
900
  ? await downloadUpdate()
870
- : await checkAndDownload();
871
- if (!downloadResult.downloaded) return downloadResult;
872
- }
901
+ : await checkAndDownload({ automatic });
902
+ if (!downloadResult.downloaded) return downloadResult;
903
+ }
904
+ if (automatic) {
905
+ const blocked = blockedAutomaticInstall();
906
+ if (blocked) return blocked;
907
+ }
908
+ installTargetVersion = status.downloadedVersion;
909
+ // Only an explicit Install action retries this failed version. A
910
+ // manual Check/download alone never grants another automatic drain.
911
+ if (!automatic) {
912
+ automaticInstallFailure = null;
913
+ updater.autoInstallOnAppQuit = true;
914
+ }
873
915
  // The updater may launch its installer synchronously from
874
916
  // quitAndInstall. Prove the exact runtime descendant tree is already
875
917
  // zero before allowing that replacement process to exist.
@@ -885,6 +927,7 @@ export function createProductUpdateManager({
885
927
  if (installRecoveryPromise) await installRecoveryPromise;
886
928
  return snapshot();
887
929
  } catch (error) {
930
+ const failure = rememberInstallerFailure(error);
888
931
  await recoverPreparedRuntime(error);
889
932
  if (!status.reopenRequired) {
890
933
  const message = String(error?.message || error);
@@ -894,7 +937,7 @@ export function createProductUpdateManager({
894
937
  error: message,
895
938
  errorCode: preinstallRecoveryFailed
896
939
  ? 'desktop-update-runtime-recovery-failed'
897
- : 'desktop-update-installer-launch-failed',
940
+ : failure.errorCode,
898
941
  reopenRequired: preinstallRecoveryFailed
899
942
  });
900
943
  }
package/hub/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/hub",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "VuvoDesk local Hub API and browser frame bridge",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -15,15 +15,15 @@ const clients = [6, 3, 1, 5, 2, 4].map(slotNumber => ({
15
15
  }));
16
16
 
17
17
  test('Hub and browser choose the same stable Free, Plus, Pro, and Team device owners', () => {
18
- for (const limit of [5, 20, 50, Number.POSITIVE_INFINITY]) {
18
+ for (const limit of [3, 20, 50, Number.POSITIVE_INFINITY]) {
19
19
  assert.deepEqual(
20
20
  stableHubPlanAllowedDeviceIds(clients, limit),
21
21
  stableBrowserPlanAllowedDeviceIds(clients, limit)
22
22
  );
23
23
  }
24
24
  assert.deepEqual(
25
- stableHubPlanAllowedDeviceIds(clients, 5),
26
- ['client-1', 'client-2', 'client-3', 'client-4', 'client-5']
25
+ stableHubPlanAllowedDeviceIds(clients, 3),
26
+ ['client-1', 'client-2', 'client-3']
27
27
  );
28
28
  });
29
29
 
@@ -66,10 +66,10 @@ test('finite plans reject the exact excess Client instead of counting request wi
66
66
 
67
67
  test('a Team to Free downgrade changes one bounded snapshot and preserves stable owners', () => {
68
68
  const unlimited = createPlanDeviceAccessSnapshot(clients, Number.POSITIVE_INFINITY, 1);
69
- const downgraded = createPlanDeviceAccessSnapshot(clients, 5, 2);
69
+ const downgraded = createPlanDeviceAccessSnapshot(clients, 3, 2);
70
70
  assert.equal(planDeviceAccessSnapshotChanged(unlimited, downgraded), true);
71
- assert.deepEqual(downgraded.allowedDeviceIds, ['client-1', 'client-2', 'client-3', 'client-4', 'client-5']);
72
- assert.deepEqual(downgraded.blockedDeviceIds, ['client-6']);
71
+ assert.deepEqual(downgraded.allowedDeviceIds, ['client-1', 'client-2', 'client-3']);
72
+ assert.deepEqual(downgraded.blockedDeviceIds, ['client-6', 'client-5', 'client-4']);
73
73
  assert.equal(planDeviceAccessSnapshotChanged(downgraded, {
74
74
  ...downgraded,
75
75
  generation: 3,
@@ -90,12 +90,12 @@ test('the exact Hub manager stays controllable without consuming a Client slot',
90
90
  const snapshot = createPlanDeviceAccessSnapshot([
91
91
  ...clients,
92
92
  { deviceId: 'hub-manager', slotNumber: 0, connected: true }
93
- ], 5, 8, { exemptDeviceIds: ['hub-manager'] });
93
+ ], 3, 8, { exemptDeviceIds: ['hub-manager'] });
94
94
 
95
95
  assert.deepEqual(snapshot.connectedDeviceIds, clients.map(client => client.deviceId));
96
96
  assert.deepEqual(snapshot.exemptConnectedDeviceIds, ['hub-manager']);
97
97
  assert.equal(snapshot.allowedDeviceIdSet.has('hub-manager'), true);
98
- assert.deepEqual(snapshot.blockedDeviceIds, ['client-6']);
98
+ assert.deepEqual(snapshot.blockedDeviceIds, ['client-6', 'client-5', 'client-4']);
99
99
  assert.deepEqual(partitionPlanDeviceIds(['hub-manager', 'client-6'], snapshot), {
100
100
  allowedDeviceIds: ['hub-manager'],
101
101
  blockedDeviceIds: ['client-6']
@@ -11621,9 +11621,18 @@ export function createRemoteHub(options = {}) {
11621
11621
  }
11622
11622
 
11623
11623
  function startLiveStream(deviceId, options = {}) {
11624
- const device = devices.get(String(deviceId || ''));
11625
- const denied = policyError(device);
11626
- if (denied) return { ok: false, error: denied };
11624
+ const device = devices.get(String(deviceId || ''));
11625
+ const denied = policyError(device);
11626
+ if (denied) return { ok: false, error: denied };
11627
+ const expectedSessionId = safeString(options.expectedSessionId, 160);
11628
+ if (expectedSessionId && device?.sessionId && expectedSessionId !== device.sessionId) {
11629
+ // A delayed Wall restoration cannot create capture on a replacement
11630
+ // Client session. This is a retired intent, not a successful start.
11631
+ return {
11632
+ ok: true, skipped: true, staleOwner: true,
11633
+ retainedNewerOwner: true, reason: 'wall-restore-session-replaced'
11634
+ };
11635
+ }
11627
11636
  if (device?.synthetic !== true
11628
11637
  && device?.connected
11629
11638
  && !isDeviceConnectionFresh(device)) {
package/hub/src/server.js CHANGED
@@ -177,7 +177,7 @@ const atlasPool = new Mode4AtlasPool({
177
177
  });
178
178
  const inputClients = new Set();
179
179
  const audioClients = new Set();
180
- const FREE_DEVICE_LIMIT = 5;
180
+ const FREE_DEVICE_LIMIT = 3;
181
181
  const PLUS_DEVICE_LIMIT = 20;
182
182
  const PRO_DEVICE_LIMIT = 50;
183
183
  const LICENSE_VERIFY_MAX_AGE_MS = 6 * 60 * 60 * 1000;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "livedesk",
3
- "version": "0.1.697",
4
- "livedeskClientVersion": "0.1.276",
3
+ "version": "0.1.701",
4
+ "livedeskClientVersion": "0.1.277",
5
5
  "buildFlavor": "production",
6
6
  "description": "VuvoDesk Hub and client launcher",
7
7
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "README.md"
24
24
  ],
25
25
  "scripts": {
26
- "check": "node --check bin/livedesk.js && node --check bootstrap/npm-update-cache.mjs && node --check bootstrap/update-host-client.mjs && node --check bootstrap/update-host.mjs && node --check diagnostics/vuvodesk-mode3-capture-smoke.mjs && node --check diagnostics/vuvodesk-mac-physical-gate-core.mjs && node --check diagnostics/vuvodesk-mac-physical-isolation.mjs && node --check diagnostics/vuvodesk-transport-physical-core.mjs && node --check diagnostics/vuvodesk-transport-physical.mjs && node --check scripts/sync-web-dist.js",
26
+ "check": "node --check bin/livedesk.js && node --check bootstrap/npm-update-cache.mjs && node --check bootstrap/update-host-client.mjs && node --check bootstrap/update-host.mjs && node --check diagnostics/vuvodesk-mode3-capture-smoke.mjs && node --check diagnostics/vuvodesk-mac-physical-gate-core.mjs && node --check diagnostics/vuvodesk-mac-physical-isolation.mjs && node --check diagnostics/vuvodesk-transport-physical-core.mjs && node --check diagnostics/vuvodesk-transport-physical.mjs && node --check diagnostics/vuvodesk-linux-desktop.mjs && node --check scripts/sync-web-dist.js",
27
27
  "build": "node scripts/sync-web-dist.js",
28
28
  "prepack": "npm --prefix ../.. run build -w @livedesk/web && node scripts/sync-web-dist.js",
29
29
  "pack:dry": "npm pack --dry-run",
@@ -54,10 +54,10 @@
54
54
  "ws": "^8.18.3"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@livedesk/fast-linux-x64": "0.1.471",
58
- "@livedesk/fast-osx-arm64": "0.1.471",
59
- "@livedesk/fast-osx-x64": "0.1.471",
60
- "@livedesk/fast-win-x64": "0.1.471"
57
+ "@livedesk/fast-linux-x64": "0.1.472",
58
+ "@livedesk/fast-osx-arm64": "0.1.472",
59
+ "@livedesk/fast-osx-x64": "0.1.472",
60
+ "@livedesk/fast-win-x64": "0.1.472"
61
61
  },
62
62
  "publishConfig": {
63
63
  "access": "public"
package/web/dist/app.html CHANGED
@@ -52,7 +52,7 @@
52
52
  .vuvodesk-static-boot [hidden] { display: none; }
53
53
  @keyframes vuvodesk-static-boot-spin { to { transform: rotate(360deg); } }
54
54
  </style>
55
- <script type="module" crossorigin src="/assets/app-DcrmhmnT.js" data-vuvodesk-entry></script>
55
+ <script type="module" crossorigin src="/assets/app-CUM2wTSb.js" data-vuvodesk-entry></script>
56
56
  <link rel="modulepreload" crossorigin href="/assets/public-site-routing-pddZbLCj.js">
57
57
  <link rel="modulepreload" crossorigin href="/assets/pwa-bootstrap-ljOQTt2e.js">
58
58
  </head>
@@ -4,7 +4,7 @@
4
4
  "short_name": "VuvoDesk",
5
5
  "description": "Monitor and control your VuvoDesk computers from your phone.",
6
6
  "lang": "en",
7
- "start_url": "/app?pwa=0.1.697",
7
+ "start_url": "/app?pwa=0.1.701",
8
8
  "scope": "/",
9
9
  "display": "standalone",
10
10
  "orientation": "any",