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.
- package/client/package.json +5 -5
- package/electron/auth-session-owner.mjs +56 -56
- package/electron/electron-builder.linux-x64.yml +34 -0
- package/electron/electron-builder.mac-arm64.yml +7 -0
- package/electron/electron-builder.mac-x64.yml +7 -0
- package/electron/electron-builder.win.yml +3 -0
- package/electron/entitlements.mac.plist +12 -0
- package/electron/main.mjs +112 -104
- package/electron/preload.cjs +10 -5
- package/electron/update-feed-resolver.mjs +65 -0
- package/electron/update-feed.json +14 -1
- package/electron/update-manager.mjs +169 -41
- package/hub/package.json +1 -1
- package/hub/src/remote-hub.js +6 -6
- package/package.json +6 -6
- package/web/dist/assets/index-Bzov0Sti.js +190 -0
- package/web/dist/assets/{index-Du6fwV95.css → index-bwoEoZuf.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/livedesk-build-evidence.json +15 -15
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/index-ZUNTrWWS.js +0 -190
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
46
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
47
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
48
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
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
|
|
@@ -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);
|
package/electron/preload.cjs
CHANGED
|
@@ -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
|
-
"
|
|
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
|
}
|