livedesk 0.1.573 → 0.1.575
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/electron/durable-auth-session-file.mjs +104 -0
- package/electron/main.mjs +75 -38
- package/package.json +1 -1
- package/web/dist/assets/{index-DtOBd3E1.js → index-D0o1IqWG.js} +15 -15
- package/web/dist/assets/{index-D2W4oK2J.css → index-D35xL7WO.css} +1 -1
- package/web/dist/desktop-auth-callback.html +50 -54
- package/web/dist/index.html +2 -2
- package/web/dist/livedesk-build-evidence.json +16 -16
- package/web/dist/sw.js +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
let temporaryFileSequence = 0;
|
|
5
|
+
|
|
6
|
+
function writeAtomicPrivateFile(path, contents) {
|
|
7
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
8
|
+
temporaryFileSequence += 1;
|
|
9
|
+
const temporaryPath = `${path}.${process.pid}.${Date.now()}.${temporaryFileSequence}.tmp`;
|
|
10
|
+
try {
|
|
11
|
+
writeFileSync(temporaryPath, contents, {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
mode: 0o600,
|
|
14
|
+
flush: true
|
|
15
|
+
});
|
|
16
|
+
renameSync(temporaryPath, path);
|
|
17
|
+
} finally {
|
|
18
|
+
try { rmSync(temporaryPath, { force: true }); } catch { /* best effort */ }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function writeDurableAuthSessionRecord({
|
|
23
|
+
primaryPath,
|
|
24
|
+
backupPath = `${primaryPath}.backup`,
|
|
25
|
+
record
|
|
26
|
+
}) {
|
|
27
|
+
if (!primaryPath || !record || typeof record !== 'object') {
|
|
28
|
+
throw new Error('durable-auth-session-write-parameters-required');
|
|
29
|
+
}
|
|
30
|
+
const contents = JSON.stringify(record, null, 2);
|
|
31
|
+
let backupError = null;
|
|
32
|
+
try {
|
|
33
|
+
writeAtomicPrivateFile(backupPath, contents);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// A mirror failure is reported to the caller, but it must not prevent the
|
|
36
|
+
// primary from preserving the successfully refreshed encrypted login.
|
|
37
|
+
backupError = error;
|
|
38
|
+
}
|
|
39
|
+
writeAtomicPrivateFile(primaryPath, contents);
|
|
40
|
+
return { backupWritten: !backupError, backupError };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readDurableAuthSessionRecord({
|
|
44
|
+
primaryPath,
|
|
45
|
+
backupPath = `${primaryPath}.backup`,
|
|
46
|
+
decode
|
|
47
|
+
}) {
|
|
48
|
+
if (!primaryPath || typeof decode !== 'function') {
|
|
49
|
+
throw new Error('durable-auth-session-read-parameters-required');
|
|
50
|
+
}
|
|
51
|
+
const failures = [];
|
|
52
|
+
const candidates = [];
|
|
53
|
+
for (const candidatePath of [primaryPath, backupPath]) {
|
|
54
|
+
if (!existsSync(candidatePath)) continue;
|
|
55
|
+
try {
|
|
56
|
+
const record = JSON.parse(readFileSync(candidatePath, 'utf8'));
|
|
57
|
+
const value = decode(record);
|
|
58
|
+
if (!value) throw new Error('durable-auth-session-record-invalid');
|
|
59
|
+
const savedAt = Date.parse(String(record?.savedAt || ''));
|
|
60
|
+
candidates.push({
|
|
61
|
+
path: candidatePath,
|
|
62
|
+
record,
|
|
63
|
+
value,
|
|
64
|
+
savedAt: Number.isFinite(savedAt) ? savedAt : 0
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
failures.push({ path: candidatePath, error: String(error?.message || error) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (candidates.length === 0) {
|
|
71
|
+
return { value: null, recoveredFromBackup: false, primaryRepaired: false, failures };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A refresh may finish writing the backup immediately before an interrupted
|
|
75
|
+
// primary replacement. Prefer the newest valid encrypted record so the app
|
|
76
|
+
// does not fall back to an older, already-rotated refresh token.
|
|
77
|
+
candidates.sort((left, right) => {
|
|
78
|
+
if (right.savedAt !== left.savedAt) return right.savedAt - left.savedAt;
|
|
79
|
+
if (left.path === primaryPath) return -1;
|
|
80
|
+
if (right.path === primaryPath) return 1;
|
|
81
|
+
return 0;
|
|
82
|
+
});
|
|
83
|
+
const selected = candidates[0];
|
|
84
|
+
const recoveredFromBackup = selected.path === backupPath;
|
|
85
|
+
let primaryRepaired = false;
|
|
86
|
+
if (recoveredFromBackup) {
|
|
87
|
+
try {
|
|
88
|
+
writeAtomicPrivateFile(primaryPath, JSON.stringify(selected.record, null, 2));
|
|
89
|
+
primaryRepaired = true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failures.push({ path: primaryPath, error: String(error?.message || error) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { value: selected.value, recoveredFromBackup, primaryRepaired, failures };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function clearDurableAuthSessionRecord({
|
|
98
|
+
primaryPath,
|
|
99
|
+
backupPath = `${primaryPath}.backup`
|
|
100
|
+
}) {
|
|
101
|
+
for (const path of [primaryPath, backupPath]) {
|
|
102
|
+
try { rmSync(path, { force: true }); } catch { /* best effort */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
package/electron/main.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, nativeImage, powerMonitor, safeStorage, shell, Tray } from 'electron';
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
@@ -7,7 +7,12 @@ import { createProductUpdateManager } from './update-manager.mjs';
|
|
|
7
7
|
import { createRecurringProductUpdateOwner } from './recurring-update-owner.mjs';
|
|
8
8
|
import { createPkceAuthorizationUrl, exchangePkceCode, fetchSupabaseUser, parsePkceCallbackUrl, refreshSupabaseSession } from './oauth-pkce.mjs';
|
|
9
9
|
import { getRuntimeAuthEnvironment, resolveDesktopAuthConfig, resolveDesktopBuildFlavor } from './auth-config.mjs';
|
|
10
|
-
import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
|
|
10
|
+
import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
|
|
11
|
+
import {
|
|
12
|
+
clearDurableAuthSessionRecord,
|
|
13
|
+
readDurableAuthSessionRecord,
|
|
14
|
+
writeDurableAuthSessionRecord
|
|
15
|
+
} from './durable-auth-session-file.mjs';
|
|
11
16
|
import {
|
|
12
17
|
DESKTOP_AUTH_RETRY_MS,
|
|
13
18
|
createDesktopAuthInvalidationGuard,
|
|
@@ -42,7 +47,8 @@ import {
|
|
|
42
47
|
// warm-idle process baseline instead of retaining a hidden utility process.
|
|
43
48
|
app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
|
|
44
49
|
|
|
45
|
-
const APP_NAME = 'LiveDesk Desktop';
|
|
50
|
+
const APP_NAME = 'LiveDesk Desktop';
|
|
51
|
+
const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
|
|
46
52
|
const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
|
|
47
53
|
const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
|
|
48
54
|
function readDesktopPackageMetadata() {
|
|
@@ -72,7 +78,8 @@ const stateRoot = configuredStateRoot
|
|
|
72
78
|
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
73
79
|
const logDir = join(stateRoot, 'logs');
|
|
74
80
|
const logPath = join(logDir, 'desktop.log');
|
|
75
|
-
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
81
|
+
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
82
|
+
const authSessionBackupPath = join(stateRoot, 'auth-session.backup.json');
|
|
76
83
|
const runtimeStateDir = process.env.LIVEDESK_STATE_DIR || join(homedir(), '.livedesk');
|
|
77
84
|
try { app.setPath('userData', stateRoot); } catch { /* Electron may reject late path changes in embedded test hosts. */ }
|
|
78
85
|
const appPath = app.getAppPath();
|
|
@@ -113,7 +120,8 @@ let runtimeLogoutPromise = null;
|
|
|
113
120
|
let desktopAuthRefreshTimer = null;
|
|
114
121
|
let desktopSessionRestorePromise = null;
|
|
115
122
|
let lastDesktopResumeRecoveryAt = 0;
|
|
116
|
-
let desktopSessionEpoch = 0;
|
|
123
|
+
let desktopSessionEpoch = 0;
|
|
124
|
+
let lastKnownDesktopSession = null;
|
|
117
125
|
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
118
126
|
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>')}`;
|
|
119
127
|
const desktopLogger = createBoundedDesktopLogger({ logPath });
|
|
@@ -177,32 +185,55 @@ function normalizeSession(session) {
|
|
|
177
185
|
};
|
|
178
186
|
}
|
|
179
187
|
|
|
180
|
-
function saveEncryptedSession(session) {
|
|
181
|
-
const normalized = normalizeSession(session);
|
|
182
|
-
if (!normalized) throw new Error('oauth-session-invalid');
|
|
183
|
-
if (!safeStorage.isEncryptionAvailable()) throw new Error('secure-storage-unavailable');
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
188
|
+
function saveEncryptedSession(session) {
|
|
189
|
+
const normalized = normalizeSession(session);
|
|
190
|
+
if (!normalized) throw new Error('oauth-session-invalid');
|
|
191
|
+
if (!safeStorage.isEncryptionAvailable()) throw new Error('secure-storage-unavailable');
|
|
192
|
+
const encryptedSession = safeStorage.encryptString(JSON.stringify(normalized)).toString('base64');
|
|
193
|
+
const persisted = writeDurableAuthSessionRecord({
|
|
194
|
+
primaryPath: authSessionPath,
|
|
195
|
+
backupPath: authSessionBackupPath,
|
|
196
|
+
record: { version: 2, savedAt: new Date().toISOString(), encryptedSession }
|
|
197
|
+
});
|
|
198
|
+
if (!persisted.backupWritten) {
|
|
199
|
+
log(`secure auth session backup write failed: ${persisted.backupError?.message || persisted.backupError}`);
|
|
200
|
+
}
|
|
201
|
+
lastKnownDesktopSession = normalized;
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readEncryptedSession() {
|
|
206
|
+
if (!safeStorage.isEncryptionAvailable()) return lastKnownDesktopSession;
|
|
207
|
+
const restored = readDurableAuthSessionRecord({
|
|
208
|
+
primaryPath: authSessionPath,
|
|
209
|
+
backupPath: authSessionBackupPath,
|
|
210
|
+
decode: saved => {
|
|
211
|
+
if (!saved?.encryptedSession) return null;
|
|
212
|
+
const session = JSON.parse(safeStorage.decryptString(Buffer.from(saved.encryptedSession, 'base64')));
|
|
213
|
+
return normalizeSession(session);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (restored.failures.length > 0) {
|
|
217
|
+
log(`secure auth session read recovered=${restored.recoveredFromBackup} failures=${restored.failures.length}`);
|
|
218
|
+
}
|
|
219
|
+
if (restored.recoveredFromBackup) log('secure auth session primary restored from encrypted backup');
|
|
220
|
+
if (!restored.value && restored.failures.length > 0) return lastKnownDesktopSession;
|
|
221
|
+
if (restored.value && !existsSync(authSessionBackupPath)) {
|
|
222
|
+
try {
|
|
223
|
+
saveEncryptedSession(restored.value);
|
|
224
|
+
log('secure auth session upgraded to atomic mirrored storage');
|
|
225
|
+
} catch (error) {
|
|
226
|
+
log(`secure auth session mirror upgrade deferred: ${error?.message || error}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
lastKnownDesktopSession = restored.value;
|
|
230
|
+
return restored.value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function clearEncryptedSession() {
|
|
234
|
+
clearDesktopAuthRefreshTimer();
|
|
235
|
+
lastKnownDesktopSession = null;
|
|
236
|
+
clearDurableAuthSessionRecord({ primaryPath: authSessionPath, backupPath: authSessionBackupPath });
|
|
206
237
|
}
|
|
207
238
|
|
|
208
239
|
function migrateLegacyPlaintextSession() {
|
|
@@ -873,11 +904,12 @@ function updateTray(status = 'Starting') {
|
|
|
873
904
|
tray.setToolTip(`${APP_NAME} — ${status}`);
|
|
874
905
|
}
|
|
875
906
|
|
|
876
|
-
function showWindow() {
|
|
877
|
-
if (!mainWindow) return;
|
|
878
|
-
mainWindow.show();
|
|
879
|
-
mainWindow.focus();
|
|
880
|
-
|
|
907
|
+
function showWindow() {
|
|
908
|
+
if (!mainWindow) return;
|
|
909
|
+
mainWindow.show();
|
|
910
|
+
mainWindow.focus();
|
|
911
|
+
requestDesktopResumeAuthRecovery('window-show');
|
|
912
|
+
}
|
|
881
913
|
|
|
882
914
|
function createMainWindow() {
|
|
883
915
|
const iconPath = appResource('electron', 'icon.png');
|
|
@@ -889,7 +921,7 @@ function createMainWindow() {
|
|
|
889
921
|
minHeight: 720,
|
|
890
922
|
show: false,
|
|
891
923
|
autoHideMenuBar: true,
|
|
892
|
-
title:
|
|
924
|
+
title: appWindowTitle(),
|
|
893
925
|
icon: existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL),
|
|
894
926
|
webPreferences: {
|
|
895
927
|
preload: appResource('electron', 'preload.cjs'),
|
|
@@ -898,7 +930,12 @@ function createMainWindow() {
|
|
|
898
930
|
sandbox: true,
|
|
899
931
|
spellcheck: false
|
|
900
932
|
}
|
|
901
|
-
});
|
|
933
|
+
});
|
|
934
|
+
mainWindow.on('page-title-updated', event => {
|
|
935
|
+
event.preventDefault();
|
|
936
|
+
mainWindow?.setTitle(appWindowTitle());
|
|
937
|
+
});
|
|
938
|
+
mainWindow.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
|
|
902
939
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
903
940
|
if (isAllowedRuntimeUrl(url)) return;
|
|
904
941
|
event.preventDefault();
|