livedesk 0.1.551 → 0.1.554

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.246",
3
+ "version": "0.1.248",
4
4
  "description": "LiveDesk 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.443",
46
- "@livedesk/fast-osx-arm64": "0.1.443",
47
- "@livedesk/fast-osx-x64": "0.1.443",
48
- "@livedesk/fast-win-x64": "0.1.443"
45
+ "@livedesk/fast-linux-x64": "0.1.445",
46
+ "@livedesk/fast-osx-arm64": "0.1.445",
47
+ "@livedesk/fast-osx-x64": "0.1.445",
48
+ "@livedesk/fast-win-x64": "0.1.445"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
@@ -1,7 +1,7 @@
1
- export const DESKTOP_AUTH_REFRESH_SKEW_SECONDS = 5 * 60;
2
- export const DESKTOP_AUTH_RETRY_MS = 15_000;
3
- export const DESKTOP_AUTH_MINIMUM_TIMER_MS = 30_000;
4
- export const DESKTOP_AUTH_INVALID_CONFIRMATIONS = 3;
1
+ export const DESKTOP_AUTH_REFRESH_SKEW_SECONDS = 5 * 60;
2
+ export const DESKTOP_AUTH_RETRY_MS = 15_000;
3
+ export const DESKTOP_AUTH_MINIMUM_TIMER_MS = 30_000;
4
+ export const DESKTOP_AUTH_INVALID_CONFIRMATIONS = 3;
5
5
 
6
6
  export function desktopSessionNeedsRefresh(
7
7
  session,
@@ -35,55 +35,55 @@ export function desktopSessionRefreshDelayMs(
35
35
  );
36
36
  }
37
37
 
38
- export function isPermanentDesktopAuthRefreshFailure(error) {
39
- const status = Number(error?.authStatus || 0);
40
- return status === 400 || status === 401 || status === 403;
41
- }
42
-
43
- export function selectFreshestDesktopSession(currentSession, incomingSession) {
44
- if (!currentSession) return incomingSession || null;
45
- if (!incomingSession) return currentSession;
46
- const currentUserId = String(currentSession.user?.id || '');
47
- const incomingUserId = String(incomingSession.user?.id || '');
48
- if (currentUserId && incomingUserId && currentUserId !== incomingUserId) return incomingSession;
49
- const currentExpiry = Number(currentSession.expires_at || 0);
50
- const incomingExpiry = Number(incomingSession.expires_at || 0);
51
- return incomingExpiry > currentExpiry ? incomingSession : currentSession;
52
- }
53
-
54
- export function createDesktopAuthInvalidationGuard({
55
- confirmations = DESKTOP_AUTH_INVALID_CONFIRMATIONS
56
- } = {}) {
57
- const required = Math.max(2, Number(confirmations) || DESKTOP_AUTH_INVALID_CONFIRMATIONS);
58
- let rejectedToken = '';
59
- let rejectionCount = 0;
60
-
61
- return {
62
- observeSuccess() {
63
- rejectedToken = '';
64
- rejectionCount = 0;
65
- },
66
- observeFailure(error, refreshToken) {
67
- if (!isPermanentDesktopAuthRefreshFailure(error)) {
68
- rejectedToken = '';
69
- rejectionCount = 0;
70
- return { permanent: false, confirmedInvalid: false, rejectionCount };
71
- }
72
- const token = String(refreshToken || '');
73
- if (!token || token !== rejectedToken) {
74
- rejectedToken = token;
75
- rejectionCount = 1;
76
- } else {
77
- rejectionCount += 1;
78
- }
79
- return {
80
- permanent: true,
81
- confirmedInvalid: rejectionCount >= required,
82
- rejectionCount
83
- };
84
- },
85
- snapshot() {
86
- return { rejectedToken, rejectionCount, confirmations: required };
87
- }
88
- };
89
- }
38
+ export function isPermanentDesktopAuthRefreshFailure(error) {
39
+ const status = Number(error?.authStatus || 0);
40
+ return status === 400 || status === 401 || status === 403;
41
+ }
42
+
43
+ export function selectFreshestDesktopSession(currentSession, incomingSession) {
44
+ if (!currentSession) return incomingSession || null;
45
+ if (!incomingSession) return currentSession;
46
+ const currentUserId = String(currentSession.user?.id || '');
47
+ const incomingUserId = String(incomingSession.user?.id || '');
48
+ if (currentUserId && incomingUserId && currentUserId !== incomingUserId) return incomingSession;
49
+ const currentExpiry = Number(currentSession.expires_at || 0);
50
+ const incomingExpiry = Number(incomingSession.expires_at || 0);
51
+ return incomingExpiry > currentExpiry ? incomingSession : currentSession;
52
+ }
53
+
54
+ export function createDesktopAuthInvalidationGuard({
55
+ confirmations = DESKTOP_AUTH_INVALID_CONFIRMATIONS
56
+ } = {}) {
57
+ const required = Math.max(2, Number(confirmations) || DESKTOP_AUTH_INVALID_CONFIRMATIONS);
58
+ let rejectedToken = '';
59
+ let rejectionCount = 0;
60
+
61
+ return {
62
+ observeSuccess() {
63
+ rejectedToken = '';
64
+ rejectionCount = 0;
65
+ },
66
+ observeFailure(error, refreshToken) {
67
+ if (!isPermanentDesktopAuthRefreshFailure(error)) {
68
+ rejectedToken = '';
69
+ rejectionCount = 0;
70
+ return { permanent: false, confirmedInvalid: false, rejectionCount };
71
+ }
72
+ const token = String(refreshToken || '');
73
+ if (!token || token !== rejectedToken) {
74
+ rejectedToken = token;
75
+ rejectionCount = 1;
76
+ } else {
77
+ rejectionCount += 1;
78
+ }
79
+ return {
80
+ permanent: true,
81
+ confirmedInvalid: rejectionCount >= required,
82
+ rejectionCount
83
+ };
84
+ },
85
+ snapshot() {
86
+ return { rejectedToken, rejectionCount, confirmations: required };
87
+ }
88
+ };
89
+ }
@@ -0,0 +1,34 @@
1
+ extends: file:packages/livedesk/electron/electron-builder.yml
2
+ files:
3
+ - bin/**
4
+ - bootstrap/**
5
+ - client/**
6
+ - electron/**
7
+ - hub/**
8
+ - runtime-core/**
9
+ - web/**
10
+ - package.json
11
+ - README.md
12
+ - "!client/fast/win-x64/**"
13
+ - "!client/fast/osx-x64/**"
14
+ - "!client/fast/osx-arm64/**"
15
+ asarUnpack:
16
+ - bin/**
17
+ - bootstrap/**
18
+ - client/**
19
+ - electron/icon.*
20
+ - electron/tray.*
21
+ - hub/**
22
+ - runtime-core/**
23
+ - web/**
24
+ linux:
25
+ icon: packages/livedesk/electron/icon.png
26
+ category: Utility
27
+ executableName: livedesk
28
+ target:
29
+ - target: AppImage
30
+ arch:
31
+ - x64
32
+ publish:
33
+ provider: generic
34
+ url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/linux/x64
@@ -24,6 +24,10 @@ asarUnpack:
24
24
  mac:
25
25
  icon: packages/livedesk/electron/icon.icns
26
26
  category: public.app-category.utilities
27
+ hardenedRuntime: true
28
+ notarize: true
29
+ entitlements: packages/livedesk/electron/entitlements.mac.plist
30
+ entitlementsInherit: packages/livedesk/electron/entitlements.mac.plist
27
31
  target:
28
32
  - target: dmg
29
33
  arch:
@@ -33,3 +37,6 @@ mac:
33
37
  - arm64
34
38
  extendInfo:
35
39
  NSMicrophoneUsageDescription: LiveDesk uses audio only when remote computer audio is enabled.
40
+ publish:
41
+ provider: generic
42
+ url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/arm64
@@ -24,6 +24,10 @@ asarUnpack:
24
24
  mac:
25
25
  icon: packages/livedesk/electron/icon.icns
26
26
  category: public.app-category.utilities
27
+ hardenedRuntime: true
28
+ notarize: true
29
+ entitlements: packages/livedesk/electron/entitlements.mac.plist
30
+ entitlementsInherit: packages/livedesk/electron/entitlements.mac.plist
27
31
  target:
28
32
  - target: dmg
29
33
  arch:
@@ -33,3 +37,6 @@ mac:
33
37
  - x64
34
38
  extendInfo:
35
39
  NSMicrophoneUsageDescription: LiveDesk uses audio only when remote computer audio is enabled.
40
+ publish:
41
+ provider: generic
42
+ url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/macos/x64
@@ -36,3 +36,6 @@ nsis:
36
36
  createStartMenuShortcut: true
37
37
  shortcutName: LiveDesk
38
38
  deleteAppDataOnUninstall: false
39
+ publish:
40
+ provider: generic
41
+ url: https://livedesk-desktop-updates.lovecrdm.workers.dev/stable/windows/x64
@@ -0,0 +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>
package/electron/main.mjs CHANGED
@@ -7,14 +7,14 @@ import { createProductUpdateManager } from './update-manager.mjs';
7
7
  import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
8
8
  import { getRuntimeAuthEnvironment, resolveDesktopAuthConfig, resolveDesktopBuildFlavor } from './auth-config.mjs';
9
9
  import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
10
- import {
11
- DESKTOP_AUTH_RETRY_MS,
12
- createDesktopAuthInvalidationGuard,
13
- desktopSessionIsUsable,
14
- desktopSessionNeedsRefresh,
15
- desktopSessionRefreshDelayMs,
16
- selectFreshestDesktopSession
17
- } from './auth-session-owner.mjs';
10
+ import {
11
+ DESKTOP_AUTH_RETRY_MS,
12
+ createDesktopAuthInvalidationGuard,
13
+ desktopSessionIsUsable,
14
+ desktopSessionNeedsRefresh,
15
+ desktopSessionRefreshDelayMs,
16
+ selectFreshestDesktopSession
17
+ } from './auth-session-owner.mjs';
18
18
  import { resolveDeviceRole } from '../bootstrap/device-role.js';
19
19
  import {
20
20
  registerOwnedWindowsChild,
@@ -102,11 +102,11 @@ let productUpdates = null;
102
102
  let pendingOAuth = null;
103
103
  let pendingProtocolUrl = '';
104
104
  let runtimeLogoutPromise = null;
105
- let desktopAuthRefreshTimer = null;
106
- let desktopSessionRestorePromise = null;
107
- let lastDesktopResumeRecoveryAt = 0;
108
- let desktopSessionEpoch = 0;
109
- const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
105
+ let desktopAuthRefreshTimer = null;
106
+ let desktopSessionRestorePromise = null;
107
+ let lastDesktopResumeRecoveryAt = 0;
108
+ let desktopSessionEpoch = 0;
109
+ const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
110
110
  const FALLBACK_ICON_DATA_URL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none"><rect x="1" y="1" width="30" height="30" rx="7" fill="#111827"/><g transform="translate(5 5) scale(.785714)"><rect x="2.5" y="4" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".42"/><path d="M7 16v1.5h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".42"/><rect x="10.5" y="2.5" width="14" height="9" rx="1.5" stroke="#F8FAFC" stroke-width="1.7" opacity=".62"/><path d="M15 14.5V16h5" stroke="#F8FAFC" stroke-width="1.7" stroke-linecap="round" opacity=".62"/><rect x="6.5" y="8.5" width="15" height="10" rx="1.7" fill="#F8FAFC" fill-opacity=".12" stroke="#F8FAFC" stroke-width="1.9"/><path d="M12 21v1.5H9.5M16 21v1.5h2.5M9.5 22.5h9" stroke="#F8FAFC" stroke-width="1.9" stroke-linecap="round"/></g></svg>')}`;
111
111
  const desktopLogger = createBoundedDesktopLogger({ logPath });
112
112
 
@@ -642,7 +642,7 @@ function clearDesktopAuthRefreshTimer() {
642
642
  desktopAuthRefreshTimer = null;
643
643
  }
644
644
 
645
- function scheduleDesktopAuthRefresh(session, delayMs = desktopSessionRefreshDelayMs(session)) {
645
+ function scheduleDesktopAuthRefresh(session, delayMs = desktopSessionRefreshDelayMs(session)) {
646
646
  clearDesktopAuthRefreshTimer();
647
647
  if (!session || isQuitting) return;
648
648
  desktopAuthRefreshTimer = setTimeout(() => {
@@ -655,53 +655,53 @@ function scheduleDesktopAuthRefresh(session, delayMs = desktopSessionRefreshDela
655
655
  log(`scheduled secure auth session recovery failed: ${error?.message || error}`);
656
656
  });
657
657
  }, Math.max(1_000, Number(delayMs) || DESKTOP_AUTH_RETRY_MS));
658
- desktopAuthRefreshTimer.unref?.();
659
- }
660
-
661
- function saveAuthoritativeDesktopSession(session) {
662
- const previous = readEncryptedSession();
663
- const saved = saveEncryptedSession(session);
664
- if (String(previous?.refresh_token || '') !== String(saved.refresh_token || '')) {
665
- desktopSessionEpoch += 1;
666
- }
667
- desktopAuthInvalidationGuard.observeSuccess();
668
- scheduleDesktopAuthRefresh(saved);
669
- return saved;
670
- }
671
-
672
- async function restoreEncryptedSessionOwned({ syncRuntime = true } = {}) {
673
- let session = readEncryptedSession();
674
- if (!session) return null;
675
- const operationEpoch = desktopSessionEpoch;
676
- if (desktopSessionNeedsRefresh(session)) {
677
- try {
678
- const refreshed = await refreshSupabaseSession({
658
+ desktopAuthRefreshTimer.unref?.();
659
+ }
660
+
661
+ function saveAuthoritativeDesktopSession(session) {
662
+ const previous = readEncryptedSession();
663
+ const saved = saveEncryptedSession(session);
664
+ if (String(previous?.refresh_token || '') !== String(saved.refresh_token || '')) {
665
+ desktopSessionEpoch += 1;
666
+ }
667
+ desktopAuthInvalidationGuard.observeSuccess();
668
+ scheduleDesktopAuthRefresh(saved);
669
+ return saved;
670
+ }
671
+
672
+ async function restoreEncryptedSessionOwned({ syncRuntime = true } = {}) {
673
+ let session = readEncryptedSession();
674
+ if (!session) return null;
675
+ const operationEpoch = desktopSessionEpoch;
676
+ if (desktopSessionNeedsRefresh(session)) {
677
+ try {
678
+ const refreshed = await refreshSupabaseSession({
679
679
  supabaseUrl: SUPABASE_URL,
680
- publishableKey: SUPABASE_PUBLISHABLE_KEY,
681
- refreshToken: session.refresh_token
682
- });
683
- if (operationEpoch !== desktopSessionEpoch) return readEncryptedSession();
684
- session = {
685
- ...session,
686
- ...refreshed,
687
- user: refreshed?.user && typeof refreshed.user === 'object' ? refreshed.user : session.user
688
- };
689
- session = saveAuthoritativeDesktopSession(session);
690
- } catch (error) {
691
- if (operationEpoch !== desktopSessionEpoch) return readEncryptedSession();
692
- log(`secure auth session refresh failed: ${error?.message || error}`);
693
- const rejection = desktopAuthInvalidationGuard.observeFailure(error, session.refresh_token);
694
- if (rejection.confirmedInvalid) {
695
- desktopSessionEpoch += 1;
696
- clearEncryptedSession();
697
- sendAuthEvent({ ok: true, signedOut: true, reason: 'refresh-rejected' });
698
- return null;
699
- }
700
- // Sleep/wake commonly resumes before DNS and Wi-Fi. Even a credential
701
- // rejection is confirmed three times so one stale or raced response
702
- // cannot destroy the durable desktop sign-in.
703
- scheduleDesktopAuthRefresh(session, DESKTOP_AUTH_RETRY_MS);
704
- return session;
680
+ publishableKey: SUPABASE_PUBLISHABLE_KEY,
681
+ refreshToken: session.refresh_token
682
+ });
683
+ if (operationEpoch !== desktopSessionEpoch) return readEncryptedSession();
684
+ session = {
685
+ ...session,
686
+ ...refreshed,
687
+ user: refreshed?.user && typeof refreshed.user === 'object' ? refreshed.user : session.user
688
+ };
689
+ session = saveAuthoritativeDesktopSession(session);
690
+ } catch (error) {
691
+ if (operationEpoch !== desktopSessionEpoch) return readEncryptedSession();
692
+ log(`secure auth session refresh failed: ${error?.message || error}`);
693
+ const rejection = desktopAuthInvalidationGuard.observeFailure(error, session.refresh_token);
694
+ if (rejection.confirmedInvalid) {
695
+ desktopSessionEpoch += 1;
696
+ clearEncryptedSession();
697
+ sendAuthEvent({ ok: true, signedOut: true, reason: 'refresh-rejected' });
698
+ return null;
699
+ }
700
+ // Sleep/wake commonly resumes before DNS and Wi-Fi. Even a credential
701
+ // rejection is confirmed three times so one stale or raced response
702
+ // cannot destroy the durable desktop sign-in.
703
+ scheduleDesktopAuthRefresh(session, DESKTOP_AUTH_RETRY_MS);
704
+ return session;
705
705
  }
706
706
  }
707
707
  if (syncRuntime) {
@@ -717,7 +717,7 @@ async function restoreEncryptedSessionOwned({ syncRuntime = true } = {}) {
717
717
  return session;
718
718
  }
719
719
 
720
- async function restoreEncryptedSession(options = {}) {
720
+ async function restoreEncryptedSession(options = {}) {
721
721
  if (desktopSessionRestorePromise) return desktopSessionRestorePromise;
722
722
  const operation = restoreEncryptedSessionOwned(options);
723
723
  desktopSessionRestorePromise = operation;
@@ -725,18 +725,18 @@ async function restoreEncryptedSession(options = {}) {
725
725
  return await operation;
726
726
  } finally {
727
727
  if (desktopSessionRestorePromise === operation) desktopSessionRestorePromise = null;
728
- }
729
- }
730
-
731
- async function signOutDesktopSession() {
732
- desktopSessionEpoch += 1;
733
- desktopAuthInvalidationGuard.observeSuccess();
734
- clearEncryptedSession();
735
- if (!runtimeLogoutPromise) {
736
- runtimeLogoutPromise = clearRuntimeSessionWithRecovery().finally(() => { runtimeLogoutPromise = null; });
737
- }
738
- return { ok: true, ...(await runtimeLogoutPromise) };
739
- }
728
+ }
729
+ }
730
+
731
+ async function signOutDesktopSession() {
732
+ desktopSessionEpoch += 1;
733
+ desktopAuthInvalidationGuard.observeSuccess();
734
+ clearEncryptedSession();
735
+ if (!runtimeLogoutPromise) {
736
+ runtimeLogoutPromise = clearRuntimeSessionWithRecovery().finally(() => { runtimeLogoutPromise = null; });
737
+ }
738
+ return { ok: true, ...(await runtimeLogoutPromise) };
739
+ }
740
740
 
741
741
  function requestDesktopResumeAuthRecovery(source) {
742
742
  const now = Date.now();
@@ -835,11 +835,11 @@ async function handleProtocolUrl(rawUrl) {
835
835
  try {
836
836
  const { code, stateVerified } = parsePkceCallbackUrl(rawUrl, request.state, { allowMissingState: true });
837
837
  if (!stateVerified) log('oauth callback omitted state; accepted only with the matching in-memory PKCE verifier');
838
- const session = saveAuthoritativeDesktopSession(await exchangePkceCode({
839
- supabaseUrl: SUPABASE_URL,
840
- publishableKey: SUPABASE_PUBLISHABLE_KEY,
841
- code,
842
- codeVerifier: request.codeVerifier
838
+ const session = saveAuthoritativeDesktopSession(await exchangePkceCode({
839
+ supabaseUrl: SUPABASE_URL,
840
+ publishableKey: SUPABASE_PUBLISHABLE_KEY,
841
+ code,
842
+ codeVerifier: request.codeVerifier
843
843
  }));
844
844
  pendingOAuth = null;
845
845
  await syncSessionToRuntime(session);
@@ -945,37 +945,37 @@ function registerIpc() {
945
945
  }
946
946
  return { ok: false, error: mask(error?.message || error) };
947
947
  }
948
- });
949
- handle('auth:sign-in-google', () => beginGoogleAuth());
950
- handle('auth:sign-out', () => signOutDesktopSession());
951
- handle('auth:get-session', () => restoreEncryptedSession({ syncRuntime: true }));
952
- handle('auth:persist-session', async session => {
953
- if (session) {
954
- try {
955
- const incoming = await validateSessionWithSupabase(session);
956
- const current = readEncryptedSession();
957
- const selected = selectFreshestDesktopSession(current, incoming);
958
- if (selected !== incoming) {
959
- scheduleDesktopAuthRefresh(selected);
960
- return { ok: true, ignoredStaleRendererSession: true };
961
- }
962
- const saved = saveAuthoritativeDesktopSession(incoming);
963
- try {
964
- await syncSessionToRuntime(saved);
965
- } catch (error) {
966
- log(`renderer auth session runtime sync deferred: ${error?.message || error}`);
967
- scheduleDesktopAuthRefresh(saved, DESKTOP_AUTH_RETRY_MS);
968
- }
969
- return { ok: true };
970
- } catch (error) {
948
+ });
949
+ handle('auth:sign-in-google', () => beginGoogleAuth());
950
+ handle('auth:sign-out', () => signOutDesktopSession());
951
+ handle('auth:get-session', () => restoreEncryptedSession({ syncRuntime: true }));
952
+ handle('auth:persist-session', async session => {
953
+ if (session) {
954
+ try {
955
+ const incoming = await validateSessionWithSupabase(session);
956
+ const current = readEncryptedSession();
957
+ const selected = selectFreshestDesktopSession(current, incoming);
958
+ if (selected !== incoming) {
959
+ scheduleDesktopAuthRefresh(selected);
960
+ return { ok: true, ignoredStaleRendererSession: true };
961
+ }
962
+ const saved = saveAuthoritativeDesktopSession(incoming);
963
+ try {
964
+ await syncSessionToRuntime(saved);
965
+ } catch (error) {
966
+ log(`renderer auth session runtime sync deferred: ${error?.message || error}`);
967
+ scheduleDesktopAuthRefresh(saved, DESKTOP_AUTH_RETRY_MS);
968
+ }
969
+ return { ok: true };
970
+ } catch (error) {
971
971
  const message = String(error?.message || error);
972
972
  log(`renderer auth session validation failed: ${message}`);
973
973
  return { ok: false, error: message };
974
974
  }
975
975
  }
976
- // Supabase may emit a null renderer session after a temporary failure.
977
- // Durable auth is cleared only by the explicit auth:sign-out IPC.
978
- return { ok: true, ignoredImplicitSignOut: true };
976
+ // Supabase may emit a null renderer session after a temporary failure.
977
+ // Durable auth is cleared only by the explicit auth:sign-out IPC.
978
+ return { ok: true, ignoredImplicitSignOut: true };
979
979
  });
980
980
  handle('app:quit', () => { app.quit(); return { ok: true }; });
981
981
  handle('runtime:get-status', async () => {
@@ -1055,7 +1055,12 @@ if (!app.requestSingleInstanceLock()) {
1055
1055
  productUpdates = createProductUpdateManager({
1056
1056
  app,
1057
1057
  onStatus: status => {
1058
+ if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
1059
+ mainWindow.webContents.send('updates:status', status);
1060
+ }
1058
1061
  if (status.state === 'available') updateTray(`Update ${status.availableVersion} available`);
1062
+ else if (status.state === 'downloading') updateTray(`Downloading update ${Math.round(status.progress || 0)}%`);
1063
+ else if (status.state === 'downloaded') updateTray(`Update ${status.availableVersion} ready`);
1059
1064
  },
1060
1065
  beforeInstall: prepareRuntimeForUpdateInstall,
1061
1066
  onInstallError: recoverRuntimeAfterUpdateInstallError
@@ -1063,6 +1068,9 @@ if (!app.requestSingleInstanceLock()) {
1063
1068
  registerIpc();
1064
1069
  powerMonitor.on('resume', () => requestDesktopResumeAuthRecovery('system-resume'));
1065
1070
  powerMonitor.on('unlock-screen', () => requestDesktopResumeAuthRecovery('screen-unlock'));
1071
+ if (DESKTOP_BUILD_FLAVOR !== 'e2e') {
1072
+ await productUpdates.checkAtStartup();
1073
+ }
1066
1074
  const startupSession = await restoreEncryptedSession({ syncRuntime: false });
1067
1075
  const forceBootstrapForE2e = DESKTOP_BUILD_FLAVOR === 'e2e' && process.env.LIVEDESK_E2E_ROLE_BOOTSTRAP === '1';
1068
1076
  const startupRole = forceBootstrapForE2e ? null : await resolveStartupRole(startupSession);
@@ -8,10 +8,10 @@ contextBridge.exposeInMainWorld('liveDesk', {
8
8
  about: () => ipcRenderer.invoke('app:about'),
9
9
  showWindow: () => ipcRenderer.invoke('app:show-window')
10
10
  },
11
- auth: {
12
- signInWithGoogle: () => ipcRenderer.invoke('auth:sign-in-google'),
13
- signOut: () => ipcRenderer.invoke('auth:sign-out'),
14
- getSession: () => ipcRenderer.invoke('auth:get-session'),
11
+ auth: {
12
+ signInWithGoogle: () => ipcRenderer.invoke('auth:sign-in-google'),
13
+ signOut: () => ipcRenderer.invoke('auth:sign-out'),
14
+ getSession: () => ipcRenderer.invoke('auth:get-session'),
15
15
  persistSession: session => ipcRenderer.invoke('auth:persist-session', session),
16
16
  onComplete: callback => {
17
17
  const listener = (_event, payload) => callback(payload);
@@ -31,6 +31,11 @@ contextBridge.exposeInMainWorld('liveDesk', {
31
31
  updates: {
32
32
  getStatus: () => ipcRenderer.invoke('updates:get-status'),
33
33
  check: () => ipcRenderer.invoke('updates:check'),
34
- install: () => ipcRenderer.invoke('updates:install')
34
+ install: () => ipcRenderer.invoke('updates:install'),
35
+ onStatus: callback => {
36
+ const listener = (_event, payload) => callback(payload);
37
+ ipcRenderer.on('updates:status', listener);
38
+ return () => ipcRenderer.removeListener('updates:status', listener);
39
+ }
35
40
  }
36
41
  });
@@ -0,0 +1,65 @@
1
+ const LOCAL_HTTP_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
2
+ const CHANNEL_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
3
+ const SEGMENT_PATTERN = /^[a-z0-9][a-z0-9/-]{0,127}$/;
4
+
5
+ function normalizeUrl(value) {
6
+ return String(value || '').trim().replace(/\/+$/, '');
7
+ }
8
+
9
+ function validateFeedUrl(value) {
10
+ const normalized = normalizeUrl(value);
11
+ if (!normalized) return { url: '', error: 'desktop-update-feed-not-configured' };
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(normalized);
15
+ } catch {
16
+ return { url: '', error: 'desktop-update-feed-invalid' };
17
+ }
18
+ const localHttp = parsed.protocol === 'http:' && LOCAL_HTTP_HOSTS.has(parsed.hostname);
19
+ if (parsed.protocol !== 'https:' && !localHttp) {
20
+ return { url: '', error: 'desktop-update-feed-insecure' };
21
+ }
22
+ if (parsed.username || parsed.password || parsed.search || parsed.hash) {
23
+ return { url: '', error: 'desktop-update-feed-invalid' };
24
+ }
25
+ return { url: normalizeUrl(parsed.toString()), error: '' };
26
+ }
27
+
28
+ export function resolveDesktopUpdateFeed({
29
+ config = {},
30
+ overrideUrl = '',
31
+ platform = process.platform,
32
+ arch = process.arch
33
+ } = {}) {
34
+ const exactOverride = validateFeedUrl(overrideUrl);
35
+ if (String(overrideUrl || '').trim()) {
36
+ return {
37
+ ...exactOverride,
38
+ configured: Boolean(exactOverride.url),
39
+ platform,
40
+ arch,
41
+ channel: 'override'
42
+ };
43
+ }
44
+
45
+ const channel = String(config?.channel || 'stable').trim().toLowerCase();
46
+ const pathSegment = String(config?.paths?.[platform]?.[arch] || '').trim().replace(/^\/+|\/+$/g, '');
47
+ if (!CHANNEL_PATTERN.test(channel)) {
48
+ return { url: '', configured: false, error: 'desktop-update-channel-invalid', platform, arch, channel };
49
+ }
50
+ if (!SEGMENT_PATTERN.test(pathSegment)) {
51
+ return { url: '', configured: false, error: 'desktop-update-platform-unsupported', platform, arch, channel };
52
+ }
53
+ const base = validateFeedUrl(config?.baseUrl);
54
+ if (!base.url) {
55
+ return { ...base, configured: false, platform, arch, channel };
56
+ }
57
+ return {
58
+ url: `${base.url}/${channel}/${pathSegment}`,
59
+ configured: true,
60
+ error: '',
61
+ platform,
62
+ arch,
63
+ channel
64
+ };
65
+ }
@@ -1,4 +1,17 @@
1
1
  {
2
2
  "provider": "generic",
3
- "url": ""
3
+ "baseUrl": "https://livedesk-desktop-updates.lovecrdm.workers.dev",
4
+ "channel": "stable",
5
+ "paths": {
6
+ "win32": {
7
+ "x64": "windows/x64"
8
+ },
9
+ "darwin": {
10
+ "x64": "macos/x64",
11
+ "arm64": "macos/arm64"
12
+ },
13
+ "linux": {
14
+ "x64": "linux/x64"
15
+ }
16
+ }
4
17
  }