livedesk 0.1.583 → 0.1.585
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 -104
- package/electron/main.mjs +77 -77
- package/hub/src/remote-hub.js +86 -86
- package/hub/src/server.js +57 -57
- package/package.json +1 -1
- package/web/dist/assets/{index-5mmX3jEZ.js → index-DGpFIbnI.js} +12 -12
- package/web/dist/desktop-auth-callback.html +42 -42
- package/web/dist/index.html +1 -1
- package/web/dist/livedesk-build-evidence.json +13 -13
- package/web/dist/sw.js +1 -1
|
@@ -1,104 +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
|
-
}
|
|
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, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
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,12 +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';
|
|
11
|
-
import {
|
|
12
|
-
clearDurableAuthSessionRecord,
|
|
13
|
-
readDurableAuthSessionRecord,
|
|
14
|
-
writeDurableAuthSessionRecord
|
|
15
|
-
} from './durable-auth-session-file.mjs';
|
|
10
|
+
import { createBoundedDesktopLogger } from './bounded-desktop-log.mjs';
|
|
11
|
+
import {
|
|
12
|
+
clearDurableAuthSessionRecord,
|
|
13
|
+
readDurableAuthSessionRecord,
|
|
14
|
+
writeDurableAuthSessionRecord
|
|
15
|
+
} from './durable-auth-session-file.mjs';
|
|
16
16
|
import {
|
|
17
17
|
DESKTOP_AUTH_RETRY_MS,
|
|
18
18
|
createDesktopAuthInvalidationGuard,
|
|
@@ -47,8 +47,8 @@ import {
|
|
|
47
47
|
// warm-idle process baseline instead of retaining a hidden utility process.
|
|
48
48
|
app.commandLine.appendSwitch('disable-features', 'AudioServiceOutOfProcess');
|
|
49
49
|
|
|
50
|
-
const APP_NAME = 'LiveDesk Desktop';
|
|
51
|
-
const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
|
|
50
|
+
const APP_NAME = 'LiveDesk Desktop';
|
|
51
|
+
const appWindowTitle = () => `${APP_NAME} · ${app.getVersion()}`;
|
|
52
52
|
const LOCAL_RUNTIME_PORT = Number(process.env.LIVEDESK_LOCAL_RUNTIME_PORT || 5179);
|
|
53
53
|
const LOCAL_RUNTIME_URL = `http://127.0.0.1:${Number.isInteger(LOCAL_RUNTIME_PORT) && LOCAL_RUNTIME_PORT > 0 ? LOCAL_RUNTIME_PORT : 5179}/`;
|
|
54
54
|
function readDesktopPackageMetadata() {
|
|
@@ -78,8 +78,8 @@ const stateRoot = configuredStateRoot
|
|
|
78
78
|
: join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'LiveDesk');
|
|
79
79
|
const logDir = join(stateRoot, 'logs');
|
|
80
80
|
const logPath = join(logDir, 'desktop.log');
|
|
81
|
-
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
82
|
-
const authSessionBackupPath = join(stateRoot, 'auth-session.backup.json');
|
|
81
|
+
const authSessionPath = join(stateRoot, 'auth-session.json');
|
|
82
|
+
const authSessionBackupPath = join(stateRoot, 'auth-session.backup.json');
|
|
83
83
|
const runtimeStateDir = process.env.LIVEDESK_STATE_DIR || join(homedir(), '.livedesk');
|
|
84
84
|
try { app.setPath('userData', stateRoot); } catch { /* Electron may reject late path changes in embedded test hosts. */ }
|
|
85
85
|
const appPath = app.getAppPath();
|
|
@@ -120,8 +120,8 @@ let runtimeLogoutPromise = null;
|
|
|
120
120
|
let desktopAuthRefreshTimer = null;
|
|
121
121
|
let desktopSessionRestorePromise = null;
|
|
122
122
|
let lastDesktopResumeRecoveryAt = 0;
|
|
123
|
-
let desktopSessionEpoch = 0;
|
|
124
|
-
let lastKnownDesktopSession = null;
|
|
123
|
+
let desktopSessionEpoch = 0;
|
|
124
|
+
let lastKnownDesktopSession = null;
|
|
125
125
|
const desktopAuthInvalidationGuard = createDesktopAuthInvalidationGuard();
|
|
126
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>')}`;
|
|
127
127
|
const desktopLogger = createBoundedDesktopLogger({ logPath });
|
|
@@ -185,55 +185,55 @@ function normalizeSession(session) {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
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 });
|
|
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 });
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
function migrateLegacyPlaintextSession() {
|
|
@@ -904,14 +904,14 @@ function updateTray(status = 'Starting') {
|
|
|
904
904
|
tray.setToolTip(`${APP_NAME} — ${status}`);
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
-
function showWindow() {
|
|
908
|
-
if (!mainWindow) return;
|
|
909
|
-
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
910
|
-
mainWindow.show();
|
|
911
|
-
mainWindow.focus();
|
|
912
|
-
if (!mainWindow.webContents.isDestroyed()) mainWindow.webContents.invalidate();
|
|
913
|
-
requestDesktopResumeAuthRecovery('window-show');
|
|
914
|
-
}
|
|
907
|
+
function showWindow() {
|
|
908
|
+
if (!mainWindow) return;
|
|
909
|
+
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
910
|
+
mainWindow.show();
|
|
911
|
+
mainWindow.focus();
|
|
912
|
+
if (!mainWindow.webContents.isDestroyed()) mainWindow.webContents.invalidate();
|
|
913
|
+
requestDesktopResumeAuthRecovery('window-show');
|
|
914
|
+
}
|
|
915
915
|
|
|
916
916
|
function createMainWindow() {
|
|
917
917
|
const iconPath = appResource('electron', 'icon.png');
|
|
@@ -923,7 +923,7 @@ function createMainWindow() {
|
|
|
923
923
|
minHeight: 720,
|
|
924
924
|
show: false,
|
|
925
925
|
autoHideMenuBar: true,
|
|
926
|
-
title: appWindowTitle(),
|
|
926
|
+
title: appWindowTitle(),
|
|
927
927
|
icon: existsSync(iconPath) ? iconPath : nativeImage.createFromDataURL(FALLBACK_ICON_DATA_URL),
|
|
928
928
|
webPreferences: {
|
|
929
929
|
preload: appResource('electron', 'preload.cjs'),
|
|
@@ -932,12 +932,12 @@ function createMainWindow() {
|
|
|
932
932
|
sandbox: true,
|
|
933
933
|
spellcheck: false
|
|
934
934
|
}
|
|
935
|
-
});
|
|
936
|
-
mainWindow.on('page-title-updated', event => {
|
|
937
|
-
event.preventDefault();
|
|
938
|
-
mainWindow?.setTitle(appWindowTitle());
|
|
939
|
-
});
|
|
940
|
-
mainWindow.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
|
|
935
|
+
});
|
|
936
|
+
mainWindow.on('page-title-updated', event => {
|
|
937
|
+
event.preventDefault();
|
|
938
|
+
mainWindow?.setTitle(appWindowTitle());
|
|
939
|
+
});
|
|
940
|
+
mainWindow.on('focus', () => requestDesktopResumeAuthRecovery('window-focus'));
|
|
941
941
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
942
942
|
if (isAllowedRuntimeUrl(url)) return;
|
|
943
943
|
event.preventDefault();
|
package/hub/src/remote-hub.js
CHANGED
|
@@ -9533,7 +9533,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9533
9533
|
return getLiveStreamIdleMs(activeLiveStream) <= getLiveStreamFreshWindowMs(activeLiveStream);
|
|
9534
9534
|
}
|
|
9535
9535
|
|
|
9536
|
-
function liveStreamMatchesOptions(activeLiveStream, normalized) {
|
|
9536
|
+
function liveStreamMatchesOptions(activeLiveStream, normalized) {
|
|
9537
9537
|
if (!activeLiveStream?.active) {
|
|
9538
9538
|
return false;
|
|
9539
9539
|
}
|
|
@@ -9544,49 +9544,49 @@ export function createRemoteHub(options = {}) {
|
|
|
9544
9544
|
&& Number(activeLiveStream.quality || 0) === normalized.quality
|
|
9545
9545
|
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
|
|
9546
9546
|
&& (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
|
|
9547
|
-
=== normalized.streamPurpose;
|
|
9548
|
-
}
|
|
9549
|
-
|
|
9550
|
-
function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
|
|
9551
|
-
if (!activeLiveStream) {
|
|
9552
|
-
return false;
|
|
9553
|
-
}
|
|
9554
|
-
return activeLiveStream.frameMode === normalized.transfer.frameMode
|
|
9555
|
-
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
|
|
9556
|
-
&& (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
|
|
9557
|
-
=== normalized.streamPurpose;
|
|
9558
|
-
}
|
|
9559
|
-
|
|
9560
|
-
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
9561
|
-
return {
|
|
9562
|
-
ok: true,
|
|
9563
|
-
commandId: activeLiveStream.commandId,
|
|
9564
|
-
sessionId: device.sessionId,
|
|
9565
|
-
streamId: activeLiveStream.streamId,
|
|
9566
|
-
streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
|
|
9567
|
-
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
9568
|
-
mode: activeLiveStream.mode || normalized.transfer.mode,
|
|
9569
|
-
frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
|
|
9570
|
-
monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
|
|
9571
|
-
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9572
|
-
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
9573
|
-
reused: true,
|
|
9574
|
-
sharedProfileReused: true,
|
|
9575
|
-
requestedProfile: {
|
|
9576
|
-
fps: normalized.fps,
|
|
9577
|
-
maxWidth: normalized.maxWidth,
|
|
9578
|
-
maxHeight: normalized.maxHeight,
|
|
9579
|
-
quality: normalized.quality
|
|
9580
|
-
},
|
|
9581
|
-
effectiveProfile: {
|
|
9582
|
-
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
9583
|
-
maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
9584
|
-
maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
|
|
9585
|
-
quality: Number(activeLiveStream.quality || normalized.quality)
|
|
9586
|
-
},
|
|
9587
|
-
...extra
|
|
9588
|
-
};
|
|
9589
|
-
}
|
|
9547
|
+
=== normalized.streamPurpose;
|
|
9548
|
+
}
|
|
9549
|
+
|
|
9550
|
+
function liveStreamMatchesOwnerIdentity(activeLiveStream, normalized) {
|
|
9551
|
+
if (!activeLiveStream) {
|
|
9552
|
+
return false;
|
|
9553
|
+
}
|
|
9554
|
+
return activeLiveStream.frameMode === normalized.transfer.frameMode
|
|
9555
|
+
&& Number(activeLiveStream.monitorIndex || 0) === normalized.monitorIndex
|
|
9556
|
+
&& (safeString(activeLiveStream.streamPurpose, 24).toLowerCase() || 'wall')
|
|
9557
|
+
=== normalized.streamPurpose;
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9560
|
+
function sharedLiveProfileResult(device, activeLiveStream, normalized, extra = {}) {
|
|
9561
|
+
return {
|
|
9562
|
+
ok: true,
|
|
9563
|
+
commandId: activeLiveStream.commandId,
|
|
9564
|
+
sessionId: device.sessionId,
|
|
9565
|
+
streamId: activeLiveStream.streamId,
|
|
9566
|
+
streamPurpose: safeString(activeLiveStream.streamPurpose, 24) || normalized.streamPurpose,
|
|
9567
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
9568
|
+
mode: activeLiveStream.mode || normalized.transfer.mode,
|
|
9569
|
+
frameMode: activeLiveStream.frameMode || normalized.transfer.frameMode,
|
|
9570
|
+
monitorIndex: Number(activeLiveStream.monitorIndex ?? normalized.monitorIndex),
|
|
9571
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9572
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
9573
|
+
reused: true,
|
|
9574
|
+
sharedProfileReused: true,
|
|
9575
|
+
requestedProfile: {
|
|
9576
|
+
fps: normalized.fps,
|
|
9577
|
+
maxWidth: normalized.maxWidth,
|
|
9578
|
+
maxHeight: normalized.maxHeight,
|
|
9579
|
+
quality: normalized.quality
|
|
9580
|
+
},
|
|
9581
|
+
effectiveProfile: {
|
|
9582
|
+
fps: Number(activeLiveStream.fps || normalized.fps),
|
|
9583
|
+
maxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
9584
|
+
maxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight),
|
|
9585
|
+
quality: Number(activeLiveStream.quality || normalized.quality)
|
|
9586
|
+
},
|
|
9587
|
+
...extra
|
|
9588
|
+
};
|
|
9589
|
+
}
|
|
9590
9590
|
|
|
9591
9591
|
function startLiveStream(deviceId, options = {}) {
|
|
9592
9592
|
const device = devices.get(String(deviceId || ''));
|
|
@@ -9742,28 +9742,28 @@ export function createRemoteHub(options = {}) {
|
|
|
9742
9742
|
const captureClaim = claimSingleLiveCapture(deviceId, device, streamId, streamPurpose);
|
|
9743
9743
|
if (!captureClaim.ok) return captureClaim;
|
|
9744
9744
|
const activeLiveStream = getDeviceLiveStream(device, streamId);
|
|
9745
|
-
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
9746
|
-
if (pendingDescriptor?.commandId) {
|
|
9747
|
-
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
9748
|
-
const pendingMatchesSharedOwner = options.forceRestart !== true
|
|
9749
|
-
&& options.reuseExisting === true
|
|
9750
|
-
&& options.reuseSharedExisting === true
|
|
9751
|
-
&& liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
|
|
9752
|
-
if ((pendingMatchesRequest || pendingMatchesSharedOwner)
|
|
9753
|
-
&& liveStreamReplacementStillPending(activeLiveStream)) {
|
|
9754
|
-
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
9745
|
+
const pendingDescriptor = getPendingLiveStreamDescriptor(activeLiveStream);
|
|
9746
|
+
if (pendingDescriptor?.commandId) {
|
|
9747
|
+
const pendingMatchesRequest = liveStreamMatchesOptions(pendingDescriptor, normalized);
|
|
9748
|
+
const pendingMatchesSharedOwner = options.forceRestart !== true
|
|
9749
|
+
&& options.reuseExisting === true
|
|
9750
|
+
&& options.reuseSharedExisting === true
|
|
9751
|
+
&& liveStreamMatchesOwnerIdentity(pendingDescriptor, normalized);
|
|
9752
|
+
if ((pendingMatchesRequest || pendingMatchesSharedOwner)
|
|
9753
|
+
&& liveStreamReplacementStillPending(activeLiveStream)) {
|
|
9754
|
+
emitRemoteEvent('RemoteLiveStreamRestartPending', device, {
|
|
9755
9755
|
streamId,
|
|
9756
9756
|
commandId: pendingDescriptor.commandId,
|
|
9757
9757
|
activeCommandId: activeLiveStream.commandId,
|
|
9758
9758
|
reason: safeString(options.restartReason || 'duplicate-restart', 128)
|
|
9759
9759
|
});
|
|
9760
|
-
if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
|
|
9761
|
-
return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
|
|
9762
|
-
ready: false,
|
|
9763
|
-
pending: true
|
|
9764
|
-
});
|
|
9765
|
-
}
|
|
9766
|
-
return {
|
|
9760
|
+
if (pendingMatchesSharedOwner && !pendingMatchesRequest) {
|
|
9761
|
+
return sharedLiveProfileResult(device, pendingDescriptor, normalized, {
|
|
9762
|
+
ready: false,
|
|
9763
|
+
pending: true
|
|
9764
|
+
});
|
|
9765
|
+
}
|
|
9766
|
+
return {
|
|
9767
9767
|
ok: true,
|
|
9768
9768
|
commandId: pendingDescriptor.commandId,
|
|
9769
9769
|
sessionId: device.sessionId,
|
|
@@ -9816,9 +9816,9 @@ export function createRemoteHub(options = {}) {
|
|
|
9816
9816
|
reused: true
|
|
9817
9817
|
};
|
|
9818
9818
|
}
|
|
9819
|
-
if (activeLiveStream
|
|
9820
|
-
&& options.forceRestart !== true
|
|
9821
|
-
&& options.reuseExisting === true
|
|
9819
|
+
if (activeLiveStream
|
|
9820
|
+
&& options.forceRestart !== true
|
|
9821
|
+
&& options.reuseExisting === true
|
|
9822
9822
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
9823
9823
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
9824
9824
|
return {
|
|
@@ -9833,28 +9833,28 @@ export function createRemoteHub(options = {}) {
|
|
|
9833
9833
|
monitorIndex: Number(activeLiveStream.monitorIndex || monitorIndex),
|
|
9834
9834
|
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9835
9835
|
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
9836
|
-
reused: true
|
|
9837
|
-
};
|
|
9838
|
-
}
|
|
9839
|
-
if (activeLiveStream
|
|
9840
|
-
&& options.forceRestart !== true
|
|
9841
|
-
&& options.reuseExisting === true
|
|
9842
|
-
&& options.reuseSharedExisting === true
|
|
9843
|
-
&& liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
|
|
9844
|
-
&& liveStreamIsReusable(activeLiveStream)) {
|
|
9845
|
-
emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
|
|
9846
|
-
streamId,
|
|
9847
|
-
commandId: activeLiveStream.commandId,
|
|
9848
|
-
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9849
|
-
requestedFps: normalized.fps,
|
|
9850
|
-
effectiveFps: Number(activeLiveStream.fps || normalized.fps),
|
|
9851
|
-
requestedMaxWidth: normalized.maxWidth,
|
|
9852
|
-
requestedMaxHeight: normalized.maxHeight,
|
|
9853
|
-
effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
9854
|
-
effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
|
|
9855
|
-
});
|
|
9856
|
-
return sharedLiveProfileResult(device, activeLiveStream, normalized);
|
|
9857
|
-
}
|
|
9836
|
+
reused: true
|
|
9837
|
+
};
|
|
9838
|
+
}
|
|
9839
|
+
if (activeLiveStream
|
|
9840
|
+
&& options.forceRestart !== true
|
|
9841
|
+
&& options.reuseExisting === true
|
|
9842
|
+
&& options.reuseSharedExisting === true
|
|
9843
|
+
&& liveStreamMatchesOwnerIdentity(activeLiveStream, normalized)
|
|
9844
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
9845
|
+
emitRemoteEvent('RemoteLiveStreamSharedProfileReused', device, {
|
|
9846
|
+
streamId,
|
|
9847
|
+
commandId: activeLiveStream.commandId,
|
|
9848
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9849
|
+
requestedFps: normalized.fps,
|
|
9850
|
+
effectiveFps: Number(activeLiveStream.fps || normalized.fps),
|
|
9851
|
+
requestedMaxWidth: normalized.maxWidth,
|
|
9852
|
+
requestedMaxHeight: normalized.maxHeight,
|
|
9853
|
+
effectiveMaxWidth: Number(activeLiveStream.maxWidth || normalized.maxWidth),
|
|
9854
|
+
effectiveMaxHeight: Number(activeLiveStream.maxHeight || normalized.maxHeight)
|
|
9855
|
+
});
|
|
9856
|
+
return sharedLiveProfileResult(device, activeLiveStream, normalized);
|
|
9857
|
+
}
|
|
9858
9858
|
if (activeLiveStream
|
|
9859
9859
|
&& options.forceRestart !== true
|
|
9860
9860
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|