@livedesk/hub 0.1.36 → 0.1.38
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/package.json +32 -32
- package/src/agents/agent-audit-store.js +6 -16
- package/src/agents/agent-permissions.js +27 -33
- package/src/agents/agent-tool-registry.js +322 -340
- package/src/captures/capture-store.js +252 -299
- package/src/filesystem/shared-folders.js +181 -189
- package/src/filesystem/transfer-jobs.js +314 -345
- package/src/live-desk-update.js +615 -683
- package/src/remote-hub.js +85 -654
- package/src/server.js +154 -965
- package/src/settings/settings-schema.js +40 -32
- package/src/settings/settings-store.js +2 -7
- package/src/transport/relay-hub-control.js +1376 -1703
- package/src/transport/udp-hub-transport.js +520 -543
- package/src/transport/udp-rendezvous.js +408 -574
- package/src/security/device-credential-authority.js +0 -406
- package/src/security/security-audit-store.js +0 -260
- package/src/transport/secure-direct-acceptor.js +0 -543
package/src/server.js
CHANGED
|
@@ -40,8 +40,7 @@ import { AgentRuntimeError } from './agents/agent-runtime-error.js';
|
|
|
40
40
|
import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
|
|
41
41
|
import { createAgentPermissionStore } from './agents/agent-permission-store.js';
|
|
42
42
|
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
43
|
-
import {
|
|
44
|
-
import { getAgentToolDefinition, isRetiredAgentMutatingToolName } from './agents/agent-tool-registry.js';
|
|
43
|
+
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
45
44
|
import { LiveDeskSettingsStore, SettingsConflictError } from './settings/settings-store.js';
|
|
46
45
|
import { effectiveDevicePolicy } from './settings/settings-schema.js';
|
|
47
46
|
import { buildEffectiveDevicePolicy } from './settings/effective-device-policy.js';
|
|
@@ -49,7 +48,6 @@ import { CaptureStore } from './captures/capture-store.js';
|
|
|
49
48
|
import { createLiveDeskUpdateManager } from './live-desk-update.js';
|
|
50
49
|
import { createHubUdpTransport } from './transport/udp-hub-transport.js';
|
|
51
50
|
import { normalizeRuntimeAuthSession, runtimeRoleError } from '../../runtime-core/src/index.js';
|
|
52
|
-
import { createOsSecretStore, OS_SECRET_REFERENCE } from '../../runtime-core/src/os-secret-store.js';
|
|
53
51
|
import { fetchAuthResponse, AUTH_REQUEST_TIMEOUT_MS } from '../../runtime-core/src/auth-http.js';
|
|
54
52
|
import { PRODUCTION_SUPABASE_PUBLISHABLE_KEY, PRODUCTION_SUPABASE_URL } from '../../runtime-core/src/auth-config.js';
|
|
55
53
|
import { createHubRuntime } from './runtime/hub-runtime.js';
|
|
@@ -67,18 +65,6 @@ const webIndexPath = resolve(webDistPath, 'index.html');
|
|
|
67
65
|
const packageInfo = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
68
66
|
const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1';
|
|
69
67
|
const httpPort = Number(process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT || 5179);
|
|
70
|
-
if (!isLoopbackBindHost(httpHost) && process.env.LIVEDESK_TEST_MODE !== '1') {
|
|
71
|
-
throw new Error('hub-http-lan-bind-requires-tls-authenticated-proxy');
|
|
72
|
-
}
|
|
73
|
-
const localAdminSessionToken = crypto.randomBytes(32).toString('base64url');
|
|
74
|
-
const localAdminCsrfToken = crypto.randomBytes(32).toString('base64url');
|
|
75
|
-
const localAdminSessionId = crypto.randomUUID();
|
|
76
|
-
const launcherShutdownToken = /^[A-Za-z0-9_-]{43}$/.test(
|
|
77
|
-
String(process.env.LIVEDESK_LAUNCHER_SHUTDOWN_TOKEN || '').trim()
|
|
78
|
-
)
|
|
79
|
-
? String(process.env.LIVEDESK_LAUNCHER_SHUTDOWN_TOKEN).trim()
|
|
80
|
-
: '';
|
|
81
|
-
const enforceLocalAdminAuth = process.env.LIVEDESK_TEST_MODE !== '1';
|
|
82
68
|
const runtimeRole = String(process.env.LIVEDESK_RUNTIME_ROLE || 'hub').trim().toLowerCase() === 'client' ? 'client' : 'hub';
|
|
83
69
|
const runtimeDeviceId = String(process.env.LIVEDESK_DEVICE_ID || '').trim();
|
|
84
70
|
const runtimeDeviceName = String(process.env.LIVEDESK_DEVICE_NAME || os.hostname()).trim() || os.hostname();
|
|
@@ -182,18 +168,10 @@ const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
|
182
168
|
const runtimeAuthStatePath = process.env.LIVEDESK_DESKTOP_HOST === '1'
|
|
183
169
|
? ''
|
|
184
170
|
: String(process.env.LIVEDESK_AUTH_STATE_PATH || '').trim();
|
|
185
|
-
const runtimeRefreshSecretStore = createOsSecretStore({
|
|
186
|
-
service: 'LiveDesk',
|
|
187
|
-
account: 'client-refresh-token',
|
|
188
|
-
dataDir: process.env.LIVEDESK_STATE_DIR || (runtimeAuthStatePath ? dirname(runtimeAuthStatePath) : undefined)
|
|
189
|
-
});
|
|
190
171
|
const testLicensePlan = process.env.LIVEDESK_TEST_MODE === '1'
|
|
191
172
|
&& ['ltd', 'pro'].includes(String(process.env.LIVEDESK_TEST_LICENSE_PLAN || '').toLowerCase())
|
|
192
173
|
? String(process.env.LIVEDESK_TEST_LICENSE_PLAN).toLowerCase()
|
|
193
174
|
: '';
|
|
194
|
-
const testSecurityAccountId = process.env.LIVEDESK_TEST_MODE === '1'
|
|
195
|
-
? String(process.env.LIVEDESK_ACCOUNT_ID || '').trim().slice(0, 128)
|
|
196
|
-
: '';
|
|
197
175
|
const persistentSessionGcEnabled =
|
|
198
176
|
process.env.LIVEDESK_TEST_MODE === '1'
|
|
199
177
|
&& process.env.LIVEDESK_PERSISTENT_SESSION_TEST_GC === '1';
|
|
@@ -207,8 +185,6 @@ let connectedDeviceCount = 0;
|
|
|
207
185
|
let runtimeAccessToken = '';
|
|
208
186
|
let runtimeRefreshToken = '';
|
|
209
187
|
let runtimeAccessTokenExpiresAt = 0;
|
|
210
|
-
let runtimeSessionGeneration = 0;
|
|
211
|
-
let runtimeRefreshPromise = null;
|
|
212
188
|
let roleWatchInFlight = false;
|
|
213
189
|
let verifiedLicense = {
|
|
214
190
|
userId: '',
|
|
@@ -222,8 +198,6 @@ let inputClientSeq = 0;
|
|
|
222
198
|
let audioClientSeq = 0;
|
|
223
199
|
let liveDeskUpdateManager = null;
|
|
224
200
|
let hubTransferJobs = null;
|
|
225
|
-
let securityAuditStore = null;
|
|
226
|
-
let securityAuditHealthy = true;
|
|
227
201
|
const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
|
|
228
202
|
const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
|
|
229
203
|
readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
|
|
@@ -270,39 +244,6 @@ function secureExactTestTokenMatches(actual, expected) {
|
|
|
270
244
|
function handleRemoteHubEvent(type, event) {
|
|
271
245
|
liveDeskUpdateManager?.handleRemoteEvent(type, event);
|
|
272
246
|
hubTransferJobs?.handleRemoteEvent(type, event);
|
|
273
|
-
const auditedRemoteActions = {
|
|
274
|
-
RemoteSecurityHandshakeAudit: event?.action || 'remote.handshake',
|
|
275
|
-
RemoteSecurityPolicyAudit: 'remote.connection-policy',
|
|
276
|
-
RemoteAbuseDefense: 'remote.abuse-defense',
|
|
277
|
-
RemoteDeviceConnected: 'remote.device.connected',
|
|
278
|
-
RemoteDeviceDisconnected: 'remote.device.disconnected',
|
|
279
|
-
RemoteInputSocketConnected: 'remote.control.channel-started',
|
|
280
|
-
RemoteInputSocketDisconnected: 'remote.control.channel-ended',
|
|
281
|
-
RemoteControlSessionStarted: 'remote.control.session-started',
|
|
282
|
-
RemoteControlSessionEnded: 'remote.control.session-ended',
|
|
283
|
-
RemoteFileSocketConnected: 'remote.file.channel-started',
|
|
284
|
-
RemoteFileSocketDisconnected: 'remote.file.channel-ended',
|
|
285
|
-
RemoteEnrollmentTokenRotated: 'remote.enrollment-token.rotated'
|
|
286
|
-
};
|
|
287
|
-
if (auditedRemoteActions[type]) {
|
|
288
|
-
const device = event?.device || {};
|
|
289
|
-
recordSecurityAudit({
|
|
290
|
-
action: auditedRemoteActions[type],
|
|
291
|
-
phase: /Disconnected|ended|rotated/i.test(`${type}:${auditedRemoteActions[type]}`) ? 'complete' : 'start',
|
|
292
|
-
result: event?.result || (event?.reason && /RemoteSecurity(?:Handshake|Policy)Audit|RemoteAbuseDefense/.test(type) ? 'rejected' : 'success'),
|
|
293
|
-
reason: event?.reason || '',
|
|
294
|
-
actorAccountId: event?.accountId || '',
|
|
295
|
-
hubId: event?.hubId || event?.remoteHub?.hostInstanceId || '',
|
|
296
|
-
deviceId: event?.deviceId || device.deviceId || '',
|
|
297
|
-
sessionId: event?.sessionId || device.sessionId || '',
|
|
298
|
-
authMethod: /RemoteSecurity(?:Handshake|Policy)Audit|RemoteAbuseDefense/.test(type) ? 'device-credential-p256' : 'authenticated-device-session',
|
|
299
|
-
details: {
|
|
300
|
-
transport: event?.transport || device.transport || '',
|
|
301
|
-
channel: event?.channel || '',
|
|
302
|
-
credentialSerial: event?.credentialSerial || ''
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
247
|
if (traceRemoteTestEventsEnabled
|
|
307
248
|
&& (type === 'RemoteFrameDropped' || type === 'RemoteFrameTransportProofAccepted')) {
|
|
308
249
|
console.warn(`[LiveDesk Hub Test Event] ${type} ${JSON.stringify({
|
|
@@ -352,12 +293,6 @@ function handleRemoteHubEvent(type, event) {
|
|
|
352
293
|
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
353
294
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
354
295
|
}
|
|
355
|
-
if (type === 'RemoteEnrollmentTokenRotated') {
|
|
356
|
-
void publishHubHostTargetWithPendingRoleTakeover('enrollment-token-rotated').catch(error => {
|
|
357
|
-
console.warn(`[LiveDesk Hub] Enrollment token publication failed: ${error?.message || error}`);
|
|
358
|
-
});
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
296
|
if (type !== 'RemoteDeviceConnected') {
|
|
362
297
|
return;
|
|
363
298
|
}
|
|
@@ -494,19 +429,7 @@ function readPersistedRuntimeSession(state = readRuntimeAuthState()) {
|
|
|
494
429
|
try {
|
|
495
430
|
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
496
431
|
const session = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
497
|
-
|
|
498
|
-
const plaintextRefreshToken = String(session.refresh_token || '').trim();
|
|
499
|
-
const refreshToken = plaintextRefreshToken || (session.refresh_token_ref === OS_SECRET_REFERENCE
|
|
500
|
-
? runtimeRefreshSecretStore.read()
|
|
501
|
-
: '');
|
|
502
|
-
if (plaintextRefreshToken) {
|
|
503
|
-
if (!runtimeRefreshSecretStore.write(plaintextRefreshToken)) return null;
|
|
504
|
-
const migrated = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
505
|
-
delete migrated.refresh_token;
|
|
506
|
-
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(migrated);
|
|
507
|
-
writePrivateRuntimeAuthState(state);
|
|
508
|
-
}
|
|
509
|
-
return { ...session, refresh_token: refreshToken };
|
|
432
|
+
return session && typeof session === 'object' ? session : null;
|
|
510
433
|
} catch {
|
|
511
434
|
return null;
|
|
512
435
|
}
|
|
@@ -550,10 +473,7 @@ function persistRuntimeSession(session) {
|
|
|
550
473
|
}
|
|
551
474
|
}, { requireRefreshToken: true });
|
|
552
475
|
if (!normalized.ok) return false;
|
|
553
|
-
|
|
554
|
-
const persisted = { ...normalized.session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
555
|
-
delete persisted.refresh_token;
|
|
556
|
-
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(persisted);
|
|
476
|
+
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(normalized.session);
|
|
557
477
|
writePrivateRuntimeAuthState(state);
|
|
558
478
|
return true;
|
|
559
479
|
}
|
|
@@ -562,13 +482,10 @@ function clearPersistedRuntimeSession() {
|
|
|
562
482
|
if (!runtimeAuthStatePath) return false;
|
|
563
483
|
const state = readRuntimeAuthState();
|
|
564
484
|
delete state[CLIENT_AUTH_STORAGE_KEY];
|
|
565
|
-
runtimeRefreshSecretStore.clear();
|
|
566
485
|
return writePrivateRuntimeAuthState(state);
|
|
567
486
|
}
|
|
568
487
|
|
|
569
488
|
function clearRuntimeSession() {
|
|
570
|
-
runtimeSessionGeneration += 1;
|
|
571
|
-
runtimeRefreshPromise = null;
|
|
572
489
|
runtimeAccessToken = '';
|
|
573
490
|
runtimeRefreshToken = '';
|
|
574
491
|
runtimeAccessTokenExpiresAt = 0;
|
|
@@ -589,137 +506,46 @@ async function getRuntimeAccessToken() {
|
|
|
589
506
|
return accessToken;
|
|
590
507
|
}
|
|
591
508
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
headers: {
|
|
602
|
-
apikey: supabasePublishableKey,
|
|
603
|
-
'Content-Type': 'application/json',
|
|
604
|
-
Accept: 'application/json'
|
|
605
|
-
},
|
|
606
|
-
body: JSON.stringify({ refresh_token: refreshToken })
|
|
509
|
+
const response = await fetchAuthResponse(
|
|
510
|
+
fetch,
|
|
511
|
+
`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`,
|
|
512
|
+
{
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: {
|
|
515
|
+
apikey: supabasePublishableKey,
|
|
516
|
+
'Content-Type': 'application/json',
|
|
517
|
+
Accept: 'application/json'
|
|
607
518
|
},
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
throw new Error(`hub-session-refresh-provider-failed:${response.status}`);
|
|
618
|
-
}
|
|
619
|
-
const refreshed = await response.json().catch(() => null);
|
|
620
|
-
const nextAccessToken = String(refreshed?.access_token || '').trim();
|
|
621
|
-
if (!nextAccessToken) {
|
|
622
|
-
throw new Error('hub-session-refresh-missing-access-token');
|
|
623
|
-
}
|
|
624
|
-
if (runtimeSessionGeneration !== ownerGeneration || runtimeRefreshToken !== refreshToken) {
|
|
625
|
-
throw new Error('hub-session-refresh-owner-retired');
|
|
626
|
-
}
|
|
627
|
-
runtimeAccessToken = nextAccessToken;
|
|
628
|
-
runtimeRefreshToken = String(refreshed?.refresh_token || refreshToken).trim();
|
|
629
|
-
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(refreshed?.expires_at)
|
|
630
|
-
|| (Number(refreshed?.expires_in) > 0 ? Date.now() + Number(refreshed.expires_in) * 1000 : 0);
|
|
631
|
-
try {
|
|
632
|
-
persistRuntimeSession({
|
|
633
|
-
access_token: runtimeAccessToken,
|
|
634
|
-
refresh_token: runtimeRefreshToken,
|
|
635
|
-
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
636
|
-
});
|
|
637
|
-
} catch (error) {
|
|
638
|
-
console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
519
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
520
|
+
},
|
|
521
|
+
SUPABASE_AUTH_TIMEOUT_MS
|
|
522
|
+
);
|
|
523
|
+
if (!response.ok) {
|
|
524
|
+
if (response.status === 400 || response.status === 401 || response.status === 403) {
|
|
525
|
+
clearRuntimeSession();
|
|
526
|
+
throw new Error(`hub-session-refresh-failed:${response.status}`);
|
|
639
527
|
}
|
|
640
|
-
|
|
641
|
-
})();
|
|
642
|
-
runtimeRefreshPromise = refreshOperation;
|
|
643
|
-
try {
|
|
644
|
-
return await refreshOperation;
|
|
645
|
-
} finally {
|
|
646
|
-
if (runtimeRefreshPromise === refreshOperation) runtimeRefreshPromise = null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async function restorePersistedHubSessionAtStartup() {
|
|
651
|
-
if (runtimeRole !== 'hub' || runtimeManager.getSnapshot().authenticated) {
|
|
652
|
-
return { ok: true, skipped: true, reason: 'not-required' };
|
|
528
|
+
throw new Error(`hub-session-refresh-provider-failed:${response.status}`);
|
|
653
529
|
}
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return { ok: false, skipped: true, reason: normalized.ok ? 'saved-user-missing' : normalized.error };
|
|
659
|
-
}
|
|
660
|
-
runtimeSessionGeneration += 1;
|
|
661
|
-
runtimeRefreshPromise = null;
|
|
662
|
-
runtimeAccessToken = String(normalized.session.access_token || '').trim();
|
|
663
|
-
runtimeRefreshToken = String(normalized.session.refresh_token || '').trim();
|
|
664
|
-
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(normalized.session.expires_at);
|
|
665
|
-
const accessToken = await getRuntimeAccessToken();
|
|
666
|
-
if (!accessToken) throw new Error('saved-hub-session-access-token-unavailable');
|
|
667
|
-
runtimeManager.setAuthenticated(true, userId);
|
|
668
|
-
persistRuntimeSession({
|
|
669
|
-
access_token: runtimeAccessToken,
|
|
670
|
-
refresh_token: runtimeRefreshToken,
|
|
671
|
-
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000),
|
|
672
|
-
user: normalized.session.user
|
|
673
|
-
});
|
|
674
|
-
const hostTarget = await publishHubHostTargetWithPendingRoleTakeover('saved-session-restored');
|
|
675
|
-
startHubHostTargetLeaseRenewal();
|
|
676
|
-
console.log(`[LiveDesk Hub] Saved sign-in restored locally; host target publication ${hostTarget?.ok === false ? 'will retry' : 'is active'}.`);
|
|
677
|
-
return { ok: true, skipped: false, hostTarget };
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function isLoopbackBindHost(value) {
|
|
681
|
-
const host = String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
682
|
-
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
async function revokeRuntimeProviderSession() {
|
|
686
|
-
const hasRuntimeSession = Boolean(runtimeAccessToken || runtimeRefreshToken);
|
|
687
|
-
if (!hasRuntimeSession) return { ok: true, skipped: true, reason: 'no-runtime-session' };
|
|
688
|
-
|
|
689
|
-
let accessToken = String(runtimeAccessToken || '').trim();
|
|
690
|
-
try {
|
|
691
|
-
accessToken = await getRuntimeAccessToken() || accessToken;
|
|
692
|
-
} catch (error) {
|
|
693
|
-
const message = String(error?.message || error);
|
|
694
|
-
if (/^hub-session-refresh-failed:(?:400|401|403)$/.test(message)) {
|
|
695
|
-
return { ok: true, alreadyInvalid: true };
|
|
696
|
-
}
|
|
697
|
-
return { ok: false, error: message };
|
|
530
|
+
const refreshed = await response.json().catch(() => null);
|
|
531
|
+
const nextAccessToken = String(refreshed?.access_token || '').trim();
|
|
532
|
+
if (!nextAccessToken) {
|
|
533
|
+
throw new Error('hub-session-refresh-missing-access-token');
|
|
698
534
|
}
|
|
699
|
-
|
|
700
|
-
|
|
535
|
+
runtimeAccessToken = nextAccessToken;
|
|
536
|
+
runtimeRefreshToken = String(refreshed?.refresh_token || refreshToken).trim();
|
|
537
|
+
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(refreshed?.expires_at)
|
|
538
|
+
|| (Number(refreshed?.expires_in) > 0 ? Date.now() + Number(refreshed.expires_in) * 1000 : 0);
|
|
701
539
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
headers: {
|
|
708
|
-
apikey: supabasePublishableKey,
|
|
709
|
-
Authorization: `Bearer ${accessToken}`,
|
|
710
|
-
Accept: 'application/json'
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
SUPABASE_AUTH_TIMEOUT_MS
|
|
714
|
-
);
|
|
715
|
-
if (response.ok) return { ok: true, revoked: true };
|
|
716
|
-
if ([401, 403, 404].includes(response.status)) {
|
|
717
|
-
return { ok: true, alreadyInvalid: true, status: response.status };
|
|
718
|
-
}
|
|
719
|
-
return { ok: false, error: `provider-session-revoke-failed:${response.status}` };
|
|
540
|
+
persistRuntimeSession({
|
|
541
|
+
access_token: runtimeAccessToken,
|
|
542
|
+
refresh_token: runtimeRefreshToken,
|
|
543
|
+
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
544
|
+
});
|
|
720
545
|
} catch (error) {
|
|
721
|
-
|
|
546
|
+
console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
722
547
|
}
|
|
548
|
+
return runtimeAccessToken;
|
|
723
549
|
}
|
|
724
550
|
|
|
725
551
|
async function verifySupabaseUser(accessToken) {
|
|
@@ -834,80 +660,14 @@ function requireHubFeatureAccess(_req, res, next) {
|
|
|
834
660
|
}
|
|
835
661
|
|
|
836
662
|
const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
837
|
-
securityAuditStore = createSecurityAuditStore({ dataDir: agentDataDir });
|
|
838
|
-
|
|
839
|
-
function requestAuditHash(value) {
|
|
840
|
-
try {
|
|
841
|
-
return crypto.createHash('sha256').update(JSON.stringify(value ?? {}), 'utf8').digest('base64url');
|
|
842
|
-
} catch {
|
|
843
|
-
return crypto.createHash('sha256').update('[unserializable]', 'utf8').digest('base64url');
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function securityAuditEvent(event = {}) {
|
|
848
|
-
const runtime = runtimeManager.getSnapshot();
|
|
849
|
-
return {
|
|
850
|
-
actorAccountId: event.actorAccountId || runtime.userId || '',
|
|
851
|
-
actorUserId: event.actorUserId || runtime.userId || '',
|
|
852
|
-
hubId: event.hubId || runtime.deviceId || '',
|
|
853
|
-
...event
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function markSecurityAuditFailed(error) {
|
|
858
|
-
if (securityAuditHealthy) {
|
|
859
|
-
securityAuditHealthy = false;
|
|
860
|
-
console.error(`[LiveDesk Hub] SECURITY AUDIT FAILURE: ${error?.message || error}`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
async function recordSecurityAuditRequired(event = {}) {
|
|
865
|
-
if (!securityAuditStore || !securityAuditHealthy) {
|
|
866
|
-
const error = new Error('security-audit-unavailable');
|
|
867
|
-
error.code = 'security-audit-unavailable';
|
|
868
|
-
throw error;
|
|
869
|
-
}
|
|
870
|
-
try {
|
|
871
|
-
return await securityAuditStore.record(securityAuditEvent(event));
|
|
872
|
-
} catch (error) {
|
|
873
|
-
markSecurityAuditFailed(error);
|
|
874
|
-
throw error;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function recordSecurityAudit(event = {}) {
|
|
879
|
-
void recordSecurityAuditRequired(event).catch(() => {
|
|
880
|
-
// Required mutation paths await recordSecurityAuditRequired directly.
|
|
881
|
-
// Best-effort lifecycle telemetry still marks the global audit gate failed.
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
|
|
885
663
|
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
886
|
-
const captureStore = new CaptureStore({
|
|
887
|
-
dataDir: agentDataDir,
|
|
888
|
-
getRetentionPolicy: () => liveDeskSettingsStore.getCached()?.filesAudio?.captureAutoDelete || 'never',
|
|
889
|
-
onRetentionDelete: result => recordSecurityAudit({
|
|
890
|
-
action: 'privacy.capture.retention-delete',
|
|
891
|
-
phase: 'complete',
|
|
892
|
-
result: 'success',
|
|
893
|
-
authMethod: 'settings-retention-policy',
|
|
894
|
-
details: result
|
|
895
|
-
})
|
|
896
|
-
});
|
|
664
|
+
const captureStore = new CaptureStore({ dataDir: agentDataDir });
|
|
897
665
|
void captureStore.initialize().catch(error => {
|
|
898
666
|
console.warn(`[LiveDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
899
667
|
});
|
|
900
|
-
void liveDeskSettingsStore.getRecord()
|
|
901
|
-
.
|
|
902
|
-
|
|
903
|
-
console.warn(`[LiveDesk Hub] settings or capture retention load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
904
|
-
});
|
|
905
|
-
const captureRetentionTimer = setInterval(() => {
|
|
906
|
-
void captureStore.enforceRetention().catch(error => {
|
|
907
|
-
console.warn(`[LiveDesk Hub] capture retention failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
908
|
-
});
|
|
909
|
-
}, 60 * 60_000);
|
|
910
|
-
captureRetentionTimer.unref?.();
|
|
668
|
+
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
669
|
+
console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
670
|
+
});
|
|
911
671
|
|
|
912
672
|
const udpTransport = createHubUdpTransport({
|
|
913
673
|
env: process.env,
|
|
@@ -918,7 +678,6 @@ const udpTransport = createHubUdpTransport({
|
|
|
918
678
|
const remoteHub = createRemoteHub({
|
|
919
679
|
managerPackage: '@livedesk/hub',
|
|
920
680
|
managerVersion: packageInfo.version,
|
|
921
|
-
dataDir: agentDataDir,
|
|
922
681
|
env: {
|
|
923
682
|
...process.env,
|
|
924
683
|
MINDEXEC_MANAGER_PACKAGE: '@livedesk/hub',
|
|
@@ -930,10 +689,6 @@ const remoteHub = createRemoteHub({
|
|
|
930
689
|
emitEvent: handleRemoteHubEvent,
|
|
931
690
|
emitFrame: broadcastRemoteBinaryFrame,
|
|
932
691
|
emitAudio: broadcastRemoteBinaryAudio,
|
|
933
|
-
getSecurityIdentity: () => ({
|
|
934
|
-
accountId: runtimeManager.getSnapshot().userId || testSecurityAccountId,
|
|
935
|
-
hubId: remoteHub?.getSecurityStatus?.().hubId || ''
|
|
936
|
-
}),
|
|
937
692
|
getEffectiveDevicePolicy: ({ deviceId, capabilities }) => buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities }),
|
|
938
693
|
getWelcomeDevicePolicy: ({ deviceId, capabilities }) => {
|
|
939
694
|
const policy = buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities });
|
|
@@ -1038,8 +793,6 @@ if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
|
|
|
1038
793
|
remoteHub,
|
|
1039
794
|
currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
|
|
1040
795
|
currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
|
|
1041
|
-
updateManifestUrl: process.env.LIVEDESK_UPDATE_MANIFEST_URL || '',
|
|
1042
|
-
updatePublicKey: process.env.LIVEDESK_UPDATE_PUBLIC_KEY || '',
|
|
1043
796
|
restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
|
|
1044
797
|
requestHubRestart
|
|
1045
798
|
});
|
|
@@ -1367,21 +1120,6 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1367
1120
|
return { ok: false, error: 'codex-tool-limit-reached' };
|
|
1368
1121
|
}
|
|
1369
1122
|
session.toolCallCount += 1;
|
|
1370
|
-
if (isRetiredAgentMutatingToolName(name)) {
|
|
1371
|
-
recordAgentAudit({
|
|
1372
|
-
event: 'tool-rejected',
|
|
1373
|
-
runId: session.runId,
|
|
1374
|
-
deviceIds: [],
|
|
1375
|
-
toolName: name,
|
|
1376
|
-
category: 'mutation',
|
|
1377
|
-
decision: 'deny',
|
|
1378
|
-
status: 'rejected',
|
|
1379
|
-
permissionMode: session.permissionPolicy?.mode,
|
|
1380
|
-
policyHash: session.permissionPolicyHash,
|
|
1381
|
-
details: { reason: 'agent-mutating-tool-disabled' }
|
|
1382
|
-
});
|
|
1383
|
-
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
1384
|
-
}
|
|
1385
1123
|
const tool = getAgentToolDefinition(name);
|
|
1386
1124
|
if (!tool) return { ok: false, error: 'codex-tool-not-allowed' };
|
|
1387
1125
|
const validationError = validateAgentMcpArguments(name, args);
|
|
@@ -1574,18 +1312,11 @@ async function synchronizeAgentEnablement() {
|
|
|
1574
1312
|
}
|
|
1575
1313
|
|
|
1576
1314
|
const hubFilesystem = createHubFilesystem();
|
|
1577
|
-
const fileTransferCommandAckTimeoutMs = readPositiveIntegerEnv(
|
|
1578
|
-
'LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS',
|
|
1579
|
-
30_000
|
|
1580
|
-
);
|
|
1581
1315
|
hubTransferJobs = createHubTransferJobs({
|
|
1582
1316
|
filesystem: hubFilesystem,
|
|
1583
1317
|
remoteHub,
|
|
1584
1318
|
maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
|
|
1585
|
-
commandResultTimeoutMs:
|
|
1586
|
-
getMaxFileSizeBytes: () => Number(
|
|
1587
|
-
liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
|
|
1588
|
-
|| 1024 * 1024 * 1024)
|
|
1319
|
+
commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
|
|
1589
1320
|
});
|
|
1590
1321
|
const hubSharedFolders = createHubSharedFolders({
|
|
1591
1322
|
filesystem: hubFilesystem,
|
|
@@ -1706,7 +1437,7 @@ app.use((req, res, next) => {
|
|
|
1706
1437
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1707
1438
|
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
1708
1439
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
1709
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization
|
|
1440
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1710
1441
|
res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
|
|
1711
1442
|
}
|
|
1712
1443
|
if (req.method === 'OPTIONS') {
|
|
@@ -1715,143 +1446,7 @@ app.use((req, res, next) => {
|
|
|
1715
1446
|
}
|
|
1716
1447
|
next();
|
|
1717
1448
|
});
|
|
1718
|
-
|
|
1719
|
-
function parseHubJsonRequest(req, res, next, onParsed = next) {
|
|
1720
|
-
parseHubJsonBody(req, res, error => {
|
|
1721
|
-
if (!error) {
|
|
1722
|
-
onParsed();
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const type = String(error?.type || '');
|
|
1726
|
-
const code = String(error?.code || '');
|
|
1727
|
-
const bodyError = type === 'entity.parse.failed'
|
|
1728
|
-
? { status: 400, error: 'invalid-json' }
|
|
1729
|
-
: type === 'entity.too.large'
|
|
1730
|
-
? { status: 413, error: 'request-too-large' }
|
|
1731
|
-
: type === 'request.aborted' || type === 'stream.not.readable' || code === 'ECONNABORTED'
|
|
1732
|
-
? { status: 400, error: 'request-body-unavailable' }
|
|
1733
|
-
: null;
|
|
1734
|
-
if (!bodyError) {
|
|
1735
|
-
next(error);
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
if (res.headersSent || res.destroyed || req.destroyed) return;
|
|
1739
|
-
res.status(bodyError.status).json({ ok: false, error: bodyError.error });
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
app.use((req, res, next) => {
|
|
1743
|
-
const mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
|
|
1744
|
-
if (!enforceLocalAdminAuth || req.path === '/api/security/session' || req.method === 'OPTIONS') {
|
|
1745
|
-
if (mutating) parseHubJsonRequest(req, res, next);
|
|
1746
|
-
else next();
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
const authorizedLauncherShutdown = req.method === 'POST'
|
|
1750
|
-
&& req.path === '/api/runtime/shutdown'
|
|
1751
|
-
&& isLoopbackAddress(req.socket?.remoteAddress)
|
|
1752
|
-
&& launcherShutdownToken
|
|
1753
|
-
&& secureExactTestTokenMatches(
|
|
1754
|
-
String(req.headers['x-livedesk-launcher-shutdown'] || ''),
|
|
1755
|
-
launcherShutdownToken
|
|
1756
|
-
);
|
|
1757
|
-
if (authorizedLauncherShutdown) {
|
|
1758
|
-
req.liveDeskLauncherShutdown = true;
|
|
1759
|
-
parseHubJsonRequest(req, res, next);
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const sensitiveRead = req.method === 'GET'
|
|
1763
|
-
&& ['/api/security/audit', '/api/security/rendezvous-issuer', '/api/privacy/inventory'].includes(req.path);
|
|
1764
|
-
if (!mutating && !sensitiveRead) {
|
|
1765
|
-
next();
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
const adminToken = String(req.headers['x-livedesk-admin-session'] || '');
|
|
1769
|
-
const csrfToken = String(req.headers['x-livedesk-admin-csrf'] || '');
|
|
1770
|
-
if (!secureExactTestTokenMatches(adminToken, localAdminSessionToken)) {
|
|
1771
|
-
recordSecurityAudit({
|
|
1772
|
-
action: 'local-admin.request',
|
|
1773
|
-
phase: 'complete',
|
|
1774
|
-
result: 'rejected',
|
|
1775
|
-
reason: 'admin-session-required',
|
|
1776
|
-
requestHash: requestAuditHash({ method: req.method, path: req.path }),
|
|
1777
|
-
authMethod: 'local-admin-session'
|
|
1778
|
-
});
|
|
1779
|
-
res.status(401).json({ ok: false, error: 'local-admin-session-required' });
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
if (mutating && !secureExactTestTokenMatches(csrfToken, localAdminCsrfToken)) {
|
|
1783
|
-
recordSecurityAudit({
|
|
1784
|
-
action: 'local-admin.request',
|
|
1785
|
-
phase: 'complete',
|
|
1786
|
-
result: 'rejected',
|
|
1787
|
-
reason: 'csrf-token-required',
|
|
1788
|
-
requestHash: requestAuditHash({ method: req.method, path: req.path }),
|
|
1789
|
-
authMethod: 'local-admin-session'
|
|
1790
|
-
});
|
|
1791
|
-
res.status(403).json({ ok: false, error: 'csrf-token-required' });
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
if (!securityAuditHealthy) {
|
|
1795
|
-
res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
req.liveDeskAdminSessionId = localAdminSessionId;
|
|
1799
|
-
if (!mutating) {
|
|
1800
|
-
next();
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
const beginMutationAudit = () => {
|
|
1805
|
-
const mutationRequestHash = requestAuditHash({ method: req.method, path: req.path });
|
|
1806
|
-
void recordSecurityAuditRequired({
|
|
1807
|
-
action: 'local-admin.mutation',
|
|
1808
|
-
phase: 'accepted',
|
|
1809
|
-
result: 'accepted',
|
|
1810
|
-
sessionId: localAdminSessionId,
|
|
1811
|
-
requestHash: mutationRequestHash,
|
|
1812
|
-
authMethod: 'local-admin-session',
|
|
1813
|
-
details: { method: req.method, path: req.path }
|
|
1814
|
-
}).then(() => {
|
|
1815
|
-
const originalEnd = res.end.bind(res);
|
|
1816
|
-
let completionStarted = false;
|
|
1817
|
-
res.end = (...args) => {
|
|
1818
|
-
if (completionStarted) return res;
|
|
1819
|
-
completionStarted = true;
|
|
1820
|
-
const statusCode = Number(res.statusCode || 200);
|
|
1821
|
-
void recordSecurityAuditRequired({
|
|
1822
|
-
action: 'local-admin.mutation',
|
|
1823
|
-
phase: 'complete',
|
|
1824
|
-
result: statusCode >= 200 && statusCode < 400 ? 'success' : 'rejected',
|
|
1825
|
-
reason: statusCode >= 400 ? `http-${statusCode}` : '',
|
|
1826
|
-
sessionId: localAdminSessionId,
|
|
1827
|
-
requestHash: mutationRequestHash,
|
|
1828
|
-
authMethod: 'local-admin-session',
|
|
1829
|
-
details: { method: req.method, path: req.path, statusCode }
|
|
1830
|
-
}).then(() => {
|
|
1831
|
-
originalEnd(...args);
|
|
1832
|
-
}).catch(error => {
|
|
1833
|
-
if (res.headersSent) {
|
|
1834
|
-
res.destroy(error);
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
const callback = [...args].reverse().find(value => typeof value === 'function');
|
|
1838
|
-
const body = JSON.stringify({ ok: false, error: 'security-audit-unavailable' });
|
|
1839
|
-
res.statusCode = 503;
|
|
1840
|
-
res.removeHeader('Content-Length');
|
|
1841
|
-
res.removeHeader('Transfer-Encoding');
|
|
1842
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1843
|
-
res.setHeader('Content-Length', Buffer.byteLength(body));
|
|
1844
|
-
originalEnd(body, 'utf8', callback);
|
|
1845
|
-
});
|
|
1846
|
-
return res;
|
|
1847
|
-
};
|
|
1848
|
-
next();
|
|
1849
|
-
}).catch(() => {
|
|
1850
|
-
res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
|
|
1851
|
-
});
|
|
1852
|
-
};
|
|
1853
|
-
parseHubJsonRequest(req, res, next, beginMutationAudit);
|
|
1854
|
-
});
|
|
1449
|
+
app.use(express.json({ limit: '32mb' }));
|
|
1855
1450
|
app.use((req, res, next) => {
|
|
1856
1451
|
if (runtimeRole === 'client' && req.path.startsWith('/api/remote')) {
|
|
1857
1452
|
res.status(403).json(roleGuardResponse(runtimeRole, 'hub'));
|
|
@@ -2006,7 +1601,6 @@ function normalizeTransferFiles(value) {
|
|
|
2006
1601
|
name,
|
|
2007
1602
|
relativePath,
|
|
2008
1603
|
size: byteLength,
|
|
2009
|
-
sha256: crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex'),
|
|
2010
1604
|
mimeType: String(entry?.type || entry?.mimeType || '').slice(0, 160),
|
|
2011
1605
|
lastModified: Number(entry?.lastModified || 0) || 0,
|
|
2012
1606
|
dataBase64
|
|
@@ -2026,31 +1620,16 @@ function normalizeTransferChunk(body = {}) {
|
|
|
2026
1620
|
const totalBytes = Math.max(0, Math.floor(Number(body.totalBytes) || 0));
|
|
2027
1621
|
const byteLength = dataBase64 ? Buffer.byteLength(dataBase64, 'base64') : 0;
|
|
2028
1622
|
const final = body.final === true;
|
|
2029
|
-
const maxFileSizeBytes = Math.max(1, Number(
|
|
2030
|
-
liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
|
|
2031
|
-
|| 1024 * 1024 * 1024));
|
|
2032
1623
|
|
|
2033
1624
|
if (!name || !relativePath || offset > totalBytes || offset + byteLength > totalBytes) {
|
|
2034
1625
|
return { ok: false, error: 'invalid-file-transfer-chunk' };
|
|
2035
1626
|
}
|
|
2036
|
-
if (totalBytes > maxFileSizeBytes) {
|
|
2037
|
-
return { ok: false, error: 'file-transfer-file-too-large', totalBytes };
|
|
2038
|
-
}
|
|
2039
1627
|
if (byteLength > MAX_FILE_TRANSFER_CHUNK_BYTES) {
|
|
2040
1628
|
return { ok: false, error: 'file-transfer-chunk-too-large', byteLength };
|
|
2041
1629
|
}
|
|
2042
1630
|
if (byteLength === 0 && !(final && totalBytes === 0 && offset === 0)) {
|
|
2043
1631
|
return { ok: false, error: 'empty-file-transfer-chunk' };
|
|
2044
1632
|
}
|
|
2045
|
-
const suppliedSha256 = String(body.sha256 || '').trim().toLowerCase();
|
|
2046
|
-
const sha256 = /^[a-f0-9]{64}$/.test(suppliedSha256)
|
|
2047
|
-
? suppliedSha256
|
|
2048
|
-
: final && offset === 0 && totalBytes === byteLength
|
|
2049
|
-
? crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex')
|
|
2050
|
-
: '';
|
|
2051
|
-
if (final && !sha256) {
|
|
2052
|
-
return { ok: false, error: 'file-transfer-sha256-required', byteLength };
|
|
2053
|
-
}
|
|
2054
1633
|
|
|
2055
1634
|
return {
|
|
2056
1635
|
ok: true,
|
|
@@ -2062,7 +1641,6 @@ function normalizeTransferChunk(body = {}) {
|
|
|
2062
1641
|
byteLength,
|
|
2063
1642
|
dataBase64,
|
|
2064
1643
|
final,
|
|
2065
|
-
sha256,
|
|
2066
1644
|
mimeType: String(body.type || body.mimeType || '').slice(0, 160),
|
|
2067
1645
|
lastModified: Number(body.lastModified || 0) || 0
|
|
2068
1646
|
}
|
|
@@ -2768,11 +2346,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2768
2346
|
&& bindingIdentity === frameLaneBindingIdentity(currentExpectedBinding)) {
|
|
2769
2347
|
expectedBinding.readySent = currentExpectedBinding?.readySent === true
|
|
2770
2348
|
|| expectedBinding.readySent === true;
|
|
2771
|
-
expectedBinding.replayedKeyFrameSeq = Math.max(
|
|
2772
|
-
0,
|
|
2773
|
-
Number(currentExpectedBinding?.replayedKeyFrameSeq || 0),
|
|
2774
|
-
Number(expectedBinding.replayedKeyFrameSeq || 0)
|
|
2775
|
-
);
|
|
2776
2349
|
ws.liveDeskExpectedStreamBindingsByDeviceId.set(normalizedDeviceId, expectedBinding);
|
|
2777
2350
|
if (!lane.bindingEpochsByDeviceId.has(normalizedDeviceId)) {
|
|
2778
2351
|
lane.bindingEpochsByDeviceId.set(normalizedDeviceId, 1);
|
|
@@ -2811,50 +2384,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2811
2384
|
return bindingIdentity;
|
|
2812
2385
|
}
|
|
2813
2386
|
|
|
2814
|
-
function replayExpectedBindingKeyFrame(ws, expectedBinding) {
|
|
2815
|
-
if (!ws || ws.readyState !== ws.OPEN || !expectedBinding) {
|
|
2816
|
-
return false;
|
|
2817
|
-
}
|
|
2818
|
-
const deviceId = String(expectedBinding.deviceId || '').trim();
|
|
2819
|
-
const currentBinding = ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
|
|
2820
|
-
? ws.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
|
|
2821
|
-
: null;
|
|
2822
|
-
const bindingIdentity = frameLaneBindingIdentity(expectedBinding);
|
|
2823
|
-
if (!deviceId
|
|
2824
|
-
|| !bindingIdentity
|
|
2825
|
-
|| bindingIdentity !== frameLaneBindingIdentity(currentBinding)) {
|
|
2826
|
-
return false;
|
|
2827
|
-
}
|
|
2828
|
-
const cached = remoteHub.getFramePayload(deviceId, 'live', {
|
|
2829
|
-
requireKeyFrame: true,
|
|
2830
|
-
streamId: expectedBinding.streamId,
|
|
2831
|
-
sessionId: expectedBinding.sessionId,
|
|
2832
|
-
commandId: expectedBinding.commandId,
|
|
2833
|
-
captureGeneration: expectedBinding.captureGeneration,
|
|
2834
|
-
streamPurpose: expectedBinding.streamPurpose,
|
|
2835
|
-
monitorIndex: expectedBinding.monitorIndex
|
|
2836
|
-
});
|
|
2837
|
-
const frameSeq = Number(cached?.frame?.frameSeq || 0);
|
|
2838
|
-
if (!cached
|
|
2839
|
-
|| cached.frame?.isKeyFrame !== true
|
|
2840
|
-
|| !Number.isSafeInteger(frameSeq)
|
|
2841
|
-
|| frameSeq <= 0
|
|
2842
|
-
|| Number(currentBinding?.replayedKeyFrameSeq || 0) === frameSeq) {
|
|
2843
|
-
return false;
|
|
2844
|
-
}
|
|
2845
|
-
currentBinding.replayedKeyFrameSeq = frameSeq;
|
|
2846
|
-
broadcastRemoteBinaryFrame({
|
|
2847
|
-
kind: 'live',
|
|
2848
|
-
deviceId,
|
|
2849
|
-
frame: cached.frame,
|
|
2850
|
-
payload: cached.payload,
|
|
2851
|
-
mimeType: cached.mimeType,
|
|
2852
|
-
byteLength: cached.byteLength,
|
|
2853
|
-
replayedForBinding: true
|
|
2854
|
-
});
|
|
2855
|
-
return true;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
2387
|
function dropQueuedFrameAtForLane(lane, dropIndex) {
|
|
2859
2388
|
const droppedBefore = lane.dropped;
|
|
2860
2389
|
const droppedItem = removeFrameLaneQueueItem(lane, dropIndex);
|
|
@@ -3181,7 +2710,7 @@ function enqueueFramePacketForClient(ws, packetOwner, meta) {
|
|
|
3181
2710
|
}
|
|
3182
2711
|
// FRAME_LANE_GENERATION_CONTRACT_END
|
|
3183
2712
|
|
|
3184
|
-
function updateFrameSubscription(ws, payload = {}) {
|
|
2713
|
+
function updateFrameSubscription(ws, payload = {}) {
|
|
3185
2714
|
const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
|
|
3186
2715
|
? new Set(ws.liveDeskDeviceIds)
|
|
3187
2716
|
: new Set();
|
|
@@ -3214,41 +2743,43 @@ function updateFrameSubscription(ws, payload = {}) {
|
|
|
3214
2743
|
const targetIds = newlySubscribedIds.length > 0 ? newlySubscribedIds : deviceIds;
|
|
3215
2744
|
for (const deviceId of targetIds.slice(0, 80)) {
|
|
3216
2745
|
startFrameSubscriptionLive(ws, 'subscribe', deviceId, { reuseExisting: true });
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
function refreshFrameSubscriptionLive(ws, payload = {}) {
|
|
3221
|
-
if (!ws || ws.readyState !== 1) {
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
|
-
const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
|
|
3225
|
-
const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
|
|
3226
|
-
const targetIds = requestedIds.length > 0
|
|
3227
|
-
? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
|
|
3228
|
-
: subscribedIds;
|
|
3229
|
-
if (targetIds.length === 0) {
|
|
3230
|
-
return;
|
|
3231
|
-
}
|
|
3232
|
-
ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
|
|
3233
|
-
payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
|
|
3234
|
-
));
|
|
3235
|
-
ws.liveDeskLiveOptions = normalizeLiveOptions({
|
|
3236
|
-
...(ws.liveDeskLiveOptions || {}),
|
|
3237
|
-
...payload,
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
function refreshFrameSubscriptionLive(ws, payload = {}) {
|
|
2750
|
+
if (!ws || ws.readyState !== 1) {
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
|
|
2754
|
+
const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
|
|
2755
|
+
const targetIds = requestedIds.length > 0
|
|
2756
|
+
? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
|
|
2757
|
+
: subscribedIds;
|
|
2758
|
+
if (targetIds.length === 0) {
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
|
|
2762
|
+
payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
|
|
2763
|
+
));
|
|
2764
|
+
ws.liveDeskLiveOptions = normalizeLiveOptions({
|
|
2765
|
+
...(ws.liveDeskLiveOptions || {}),
|
|
2766
|
+
...payload,
|
|
2767
|
+
forceRestart: false,
|
|
2768
|
+
reuseExisting: true,
|
|
2769
|
+
monitorSelections: {
|
|
2770
|
+
...(ws.liveDeskLiveOptions?.monitorSelections || {}),
|
|
2771
|
+
...(payload.monitorSelections || {})
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
for (const deviceId of targetIds.slice(0, 80)) {
|
|
2775
|
+
startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
|
|
2776
|
+
...ws.liveDeskLiveOptions,
|
|
2777
|
+
reuseExisting: true
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function startFrameSubscriptionLive(
|
|
3252
2783
|
ws,
|
|
3253
2784
|
reason = 'subscribe',
|
|
3254
2785
|
onlyDeviceId = '',
|
|
@@ -3348,7 +2879,6 @@ function startFrameSubscriptionLive(
|
|
|
3348
2879
|
continue;
|
|
3349
2880
|
}
|
|
3350
2881
|
cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
|
|
3351
|
-
const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
|
|
3352
2882
|
started.push({
|
|
3353
2883
|
deviceId: expectedBinding.deviceId,
|
|
3354
2884
|
sessionId: expectedBinding.sessionId,
|
|
@@ -3357,8 +2887,7 @@ function startFrameSubscriptionLive(
|
|
|
3357
2887
|
commandId: expectedBinding.commandId,
|
|
3358
2888
|
captureGeneration: expectedBinding.captureGeneration,
|
|
3359
2889
|
monitorIndex: expectedBinding.monitorIndex,
|
|
3360
|
-
ready:
|
|
3361
|
-
replayedKeyFrame,
|
|
2890
|
+
ready: reusedFrameReady,
|
|
3362
2891
|
fps: result.fps,
|
|
3363
2892
|
reused: result.reused === true
|
|
3364
2893
|
});
|
|
@@ -4186,22 +3715,6 @@ app.get('/api/health', (_req, res) => {
|
|
|
4186
3715
|
res.json(buildHubHealthPayload());
|
|
4187
3716
|
});
|
|
4188
3717
|
|
|
4189
|
-
app.get('/api/security/session', (req, res) => {
|
|
4190
|
-
noStore(res);
|
|
4191
|
-
if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostname(requestHostname(req))) {
|
|
4192
|
-
res.status(403).json({ ok: false, error: 'local-admin-session-loopback-only' });
|
|
4193
|
-
return;
|
|
4194
|
-
}
|
|
4195
|
-
res.json({
|
|
4196
|
-
ok: true,
|
|
4197
|
-
sessionId: localAdminSessionId,
|
|
4198
|
-
adminToken: localAdminSessionToken,
|
|
4199
|
-
csrfToken: localAdminCsrfToken,
|
|
4200
|
-
issuedAt: new Date().toISOString(),
|
|
4201
|
-
expiresOnProcessExit: true
|
|
4202
|
-
});
|
|
4203
|
-
});
|
|
4204
|
-
|
|
4205
3718
|
if (persistentSessionGcEnabled) {
|
|
4206
3719
|
app.post('/api/test/persistent-session/gc', (req, res) => {
|
|
4207
3720
|
noStore(res);
|
|
@@ -4255,15 +3768,6 @@ app.patch('/api/settings', async (req, res) => {
|
|
|
4255
3768
|
await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
|
|
4256
3769
|
}
|
|
4257
3770
|
remoteHub.refreshDevicePolicies();
|
|
4258
|
-
void captureStore.enforceRetention().catch(() => undefined);
|
|
4259
|
-
recordSecurityAudit({
|
|
4260
|
-
action: 'settings.security-policy.updated',
|
|
4261
|
-
phase: 'complete',
|
|
4262
|
-
result: 'success',
|
|
4263
|
-
requestHash: requestAuditHash(patch),
|
|
4264
|
-
authMethod: 'local-admin-session',
|
|
4265
|
-
details: { revision: record.revision, sections: Object.keys(patch) }
|
|
4266
|
-
});
|
|
4267
3771
|
res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
|
|
4268
3772
|
} catch (error) {
|
|
4269
3773
|
if (error instanceof SettingsConflictError) {
|
|
@@ -4434,124 +3938,10 @@ app.get('/api/security/trusted-devices', (_req, res) => {
|
|
|
4434
3938
|
res.json({ ok: true, devices });
|
|
4435
3939
|
});
|
|
4436
3940
|
|
|
4437
|
-
app.get('/api/security/rendezvous-issuer', (_req, res) => {
|
|
4438
|
-
noStore(res);
|
|
4439
|
-
const security = remoteHub.getSecurityStatus();
|
|
4440
|
-
res.json({
|
|
4441
|
-
ok: true,
|
|
4442
|
-
hubId: security.hubId,
|
|
4443
|
-
issuerKeyId: security.hubIssuerKeyId,
|
|
4444
|
-
publicKey: security.hubPublicKey,
|
|
4445
|
-
algorithm: 'P-256/SHA-256',
|
|
4446
|
-
environmentVariable: 'LIVEDESK_UDP_RENDEZVOUS_TRUSTED_ISSUER_PUBLIC_KEYS'
|
|
4447
|
-
});
|
|
4448
|
-
});
|
|
4449
|
-
|
|
4450
|
-
app.get('/api/security/audit', async (req, res) => {
|
|
4451
|
-
noStore(res);
|
|
4452
|
-
try {
|
|
4453
|
-
res.json({
|
|
4454
|
-
ok: true,
|
|
4455
|
-
healthy: securityAuditHealthy,
|
|
4456
|
-
verification: await securityAuditStore.verify(),
|
|
4457
|
-
audit: await securityAuditStore.list({ limit: req.query?.limit, action: req.query?.action })
|
|
4458
|
-
});
|
|
4459
|
-
} catch (error) {
|
|
4460
|
-
securityAuditHealthy = false;
|
|
4461
|
-
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
4462
|
-
}
|
|
4463
|
-
});
|
|
4464
|
-
|
|
4465
|
-
app.get('/api/privacy/inventory', async (_req, res) => {
|
|
4466
|
-
noStore(res);
|
|
4467
|
-
try {
|
|
4468
|
-
const [captures, sharedFolders, verification] = await Promise.all([
|
|
4469
|
-
captureStore.list(),
|
|
4470
|
-
hubSharedFolders.list(),
|
|
4471
|
-
securityAuditStore.verify()
|
|
4472
|
-
]);
|
|
4473
|
-
res.json({
|
|
4474
|
-
ok: true,
|
|
4475
|
-
inventory: {
|
|
4476
|
-
authenticated: runtimeManager.getSnapshot().authenticated === true,
|
|
4477
|
-
captures: captures.length,
|
|
4478
|
-
sharedFolderDefinitions: sharedFolders.length,
|
|
4479
|
-
trustedDeviceCredentials: remoteHub.getSecurityStatus().devices.length,
|
|
4480
|
-
securityAuditRecords: verification.records,
|
|
4481
|
-
agentAuditRetained: (await agentAuditStore.list({ limit: 500 })).length
|
|
4482
|
-
}
|
|
4483
|
-
});
|
|
4484
|
-
} catch (error) {
|
|
4485
|
-
res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-inventory-unavailable').slice(0, 120) });
|
|
4486
|
-
}
|
|
4487
|
-
});
|
|
4488
|
-
|
|
4489
|
-
app.delete('/api/privacy/local-data', async (req, res) => {
|
|
4490
|
-
noStore(res);
|
|
4491
|
-
if (String(req.body?.confirmation || '') !== 'DELETE-LIVEDESK-LOCAL-DATA') {
|
|
4492
|
-
res.status(400).json({ ok: false, error: 'privacy-delete-confirmation-required' });
|
|
4493
|
-
return;
|
|
4494
|
-
}
|
|
4495
|
-
try {
|
|
4496
|
-
const providerLogout = await revokeRuntimeProviderSession();
|
|
4497
|
-
const hostTarget = runtimeRole === 'hub'
|
|
4498
|
-
? await clearHubHostTarget('privacy-delete')
|
|
4499
|
-
: { ok: true, active: false };
|
|
4500
|
-
clearRuntimeSession();
|
|
4501
|
-
for (const device of remoteHub.getSecurityStatus().devices) {
|
|
4502
|
-
remoteHub.disconnectDevice(device.deviceId, 'privacy-local-data-delete');
|
|
4503
|
-
}
|
|
4504
|
-
const trustedDeviceCredentials = remoteHub.clearDeviceCredentials();
|
|
4505
|
-
const transferJobs = hubTransferJobs.clear();
|
|
4506
|
-
const captures = await captureStore.removeAll();
|
|
4507
|
-
const sharedFolders = await hubSharedFolders.clear();
|
|
4508
|
-
await agentAuditStore.clear();
|
|
4509
|
-
await securityAuditStore.reset();
|
|
4510
|
-
securityAuditHealthy = true;
|
|
4511
|
-
await securityAuditStore.record({
|
|
4512
|
-
action: 'privacy.local-data.deleted',
|
|
4513
|
-
phase: 'complete',
|
|
4514
|
-
result: 'success',
|
|
4515
|
-
authMethod: 'local-admin-session',
|
|
4516
|
-
details: {
|
|
4517
|
-
capturesRemoved: captures.removed,
|
|
4518
|
-
captureBytesRemoved: captures.removedBytes,
|
|
4519
|
-
sharedFolderDefinitionsRemoved: sharedFolders.removed,
|
|
4520
|
-
trustedDeviceCredentialsRemoved: trustedDeviceCredentials,
|
|
4521
|
-
transferJobRecordsRemoved: transferJobs.removed,
|
|
4522
|
-
providerSessionRevoked: providerLogout.revoked === true
|
|
4523
|
-
}
|
|
4524
|
-
});
|
|
4525
|
-
res.json({
|
|
4526
|
-
ok: true,
|
|
4527
|
-
localDeleted: true,
|
|
4528
|
-
providerLogout,
|
|
4529
|
-
hostTarget,
|
|
4530
|
-
captures,
|
|
4531
|
-
sharedFolders,
|
|
4532
|
-
trustedDeviceCredentials,
|
|
4533
|
-
transferJobs,
|
|
4534
|
-
preserved: ['settings-preferences', 'hub-device-identity', 'application-binaries']
|
|
4535
|
-
});
|
|
4536
|
-
} catch (error) {
|
|
4537
|
-
securityAuditHealthy = false;
|
|
4538
|
-
res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-delete-failed').slice(0, 120) });
|
|
4539
|
-
}
|
|
4540
|
-
});
|
|
4541
|
-
|
|
4542
3941
|
app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
|
|
4543
3942
|
noStore(res);
|
|
4544
|
-
const
|
|
4545
|
-
|
|
4546
|
-
const revoked = disconnected === true || credentialRevoked === true;
|
|
4547
|
-
recordSecurityAudit({
|
|
4548
|
-
action: 'security.device.revoked',
|
|
4549
|
-
phase: 'complete',
|
|
4550
|
-
result: revoked ? 'success' : 'not-found',
|
|
4551
|
-
deviceId: req.params.deviceId,
|
|
4552
|
-
authMethod: 'local-admin-session'
|
|
4553
|
-
});
|
|
4554
|
-
res.json({ ok: revoked, revoked, deviceId: req.params.deviceId });
|
|
3943
|
+
const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
|
|
3944
|
+
res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
|
|
4555
3945
|
});
|
|
4556
3946
|
|
|
4557
3947
|
app.post('/api/security/revoke-all', (_req, res) => {
|
|
@@ -4559,11 +3949,8 @@ app.post('/api/security/revoke-all', (_req, res) => {
|
|
|
4559
3949
|
const devices = remoteHub.listDevices({ includeDataUrl: false });
|
|
4560
3950
|
let revoked = 0;
|
|
4561
3951
|
for (const device of devices) {
|
|
4562
|
-
|
|
4563
|
-
const credentialRevoked = remoteHub.revokeDeviceCredential(device.deviceId, 'trusted-devices-revoked');
|
|
4564
|
-
if (disconnected || credentialRevoked) revoked += 1;
|
|
3952
|
+
if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
|
|
4565
3953
|
}
|
|
4566
|
-
recordSecurityAudit({ action: 'security.devices.revoke-all', phase: 'complete', result: 'success', authMethod: 'local-admin-session', details: { revoked } });
|
|
4567
3954
|
res.json({ ok: true, revoked });
|
|
4568
3955
|
});
|
|
4569
3956
|
|
|
@@ -4927,16 +4314,8 @@ app.post('/api/runtime/restart', (_req, res) => {
|
|
|
4927
4314
|
}, 150);
|
|
4928
4315
|
});
|
|
4929
4316
|
|
|
4930
|
-
app.post('/api/runtime/shutdown', (
|
|
4317
|
+
app.post('/api/runtime/shutdown', (_req, res) => {
|
|
4931
4318
|
noStore(res);
|
|
4932
|
-
recordSecurityAudit({
|
|
4933
|
-
action: 'runtime.shutdown',
|
|
4934
|
-
phase: 'requested',
|
|
4935
|
-
result: 'accepted',
|
|
4936
|
-
authMethod: req.liveDeskLauncherShutdown === true
|
|
4937
|
-
? 'launcher-internal-shutdown-token'
|
|
4938
|
-
: 'local-admin-session'
|
|
4939
|
-
});
|
|
4940
4319
|
runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
|
|
4941
4320
|
res.json({ ok: true, shuttingDown: true, role: runtimeRole });
|
|
4942
4321
|
setTimeout(() => {
|
|
@@ -4975,8 +4354,6 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
4975
4354
|
const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
|
|
4976
4355
|
try {
|
|
4977
4356
|
const user = await verifySupabaseUser(accessToken);
|
|
4978
|
-
runtimeSessionGeneration += 1;
|
|
4979
|
-
runtimeRefreshPromise = null;
|
|
4980
4357
|
runtimeAccessToken = accessToken;
|
|
4981
4358
|
runtimeRefreshToken = refreshToken;
|
|
4982
4359
|
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
|
|
@@ -4993,26 +4370,9 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
4993
4370
|
if (runtimeRole === 'hub') {
|
|
4994
4371
|
startHubHostTargetLeaseRenewal();
|
|
4995
4372
|
}
|
|
4996
|
-
recordSecurityAudit({
|
|
4997
|
-
action: 'auth.session.established',
|
|
4998
|
-
phase: 'complete',
|
|
4999
|
-
result: 'success',
|
|
5000
|
-
actorAccountId: user.id,
|
|
5001
|
-
actorUserId: user.id,
|
|
5002
|
-
authMethod: 'supabase-access-refresh',
|
|
5003
|
-
requestHash: requestAuditHash({ userId: user.id, expiresAt })
|
|
5004
|
-
});
|
|
5005
4373
|
res.json({ ok: true, authenticated: true, persisted, userId: user.id, role: runtimeRole, hostTarget });
|
|
5006
4374
|
} catch (error) {
|
|
5007
4375
|
const message = error instanceof Error ? error.message : String(error);
|
|
5008
|
-
recordSecurityAudit({
|
|
5009
|
-
action: 'auth.session.establish',
|
|
5010
|
-
phase: 'complete',
|
|
5011
|
-
result: 'rejected',
|
|
5012
|
-
reason: message,
|
|
5013
|
-
authMethod: 'supabase-access-refresh',
|
|
5014
|
-
requestHash: requestAuditHash({ hasAccessToken: Boolean(req.body?.accessToken || req.body?.access_token), hasRefreshToken: Boolean(req.body?.refreshToken || req.body?.refresh_token) })
|
|
5015
|
-
});
|
|
5016
4376
|
const status = authVerificationHttpStatus(message);
|
|
5017
4377
|
res.status(status).json({ ok: false, error: message });
|
|
5018
4378
|
}
|
|
@@ -5308,30 +4668,11 @@ function startHubHostTargetLeaseRenewal() {
|
|
|
5308
4668
|
|
|
5309
4669
|
app.delete('/api/auth/session', async (_req, res) => {
|
|
5310
4670
|
noStore(res);
|
|
5311
|
-
const actorUserId = runtimeManager.getSnapshot().userId || '';
|
|
5312
4671
|
const hostTarget = runtimeRole === 'hub'
|
|
5313
4672
|
? await clearHubHostTarget('logout')
|
|
5314
4673
|
: { ok: true, active: false };
|
|
5315
|
-
const providerLogout = await revokeRuntimeProviderSession();
|
|
5316
4674
|
clearRuntimeSession();
|
|
5317
|
-
|
|
5318
|
-
action: 'auth.session.revoked',
|
|
5319
|
-
phase: 'complete',
|
|
5320
|
-
result: providerLogout.ok ? 'success' : 'provider-error',
|
|
5321
|
-
actorAccountId: actorUserId,
|
|
5322
|
-
actorUserId,
|
|
5323
|
-
reason: providerLogout.error || '',
|
|
5324
|
-
authMethod: 'supabase-local-signout',
|
|
5325
|
-
details: { localCleared: true, providerRevoked: providerLogout.revoked === true, alreadyInvalid: providerLogout.alreadyInvalid === true }
|
|
5326
|
-
});
|
|
5327
|
-
res.status(providerLogout.ok ? 200 : 502).json({
|
|
5328
|
-
ok: providerLogout.ok,
|
|
5329
|
-
authenticated: false,
|
|
5330
|
-
localCleared: true,
|
|
5331
|
-
role: runtimeRole,
|
|
5332
|
-
hostTarget,
|
|
5333
|
-
providerLogout
|
|
5334
|
-
});
|
|
4675
|
+
res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
|
|
5335
4676
|
});
|
|
5336
4677
|
|
|
5337
4678
|
app.get('/api/hub/status', (_req, res) => {
|
|
@@ -5682,33 +5023,7 @@ app.post('/api/remote/files/from-hub', requireHubFeatureAccess, (req, res) => {
|
|
|
5682
5023
|
const job = hubTransferJobs.create({
|
|
5683
5024
|
itemIds,
|
|
5684
5025
|
deviceIds,
|
|
5685
|
-
remoteDirectory: req.body?.remoteDirectory
|
|
5686
|
-
onComplete: async ({ completed, job: completedJob }) => {
|
|
5687
|
-
await recordSecurityAuditRequired({
|
|
5688
|
-
action: 'remote.file.job',
|
|
5689
|
-
phase: 'complete',
|
|
5690
|
-
result: completed ? 'success' : 'failed',
|
|
5691
|
-
reason: completed ? '' : completedJob?.error || 'file-transfer-failed',
|
|
5692
|
-
deviceIds,
|
|
5693
|
-
sessionId: completedJob?.jobId || '',
|
|
5694
|
-
requestHash: requestAuditHash({
|
|
5695
|
-
jobId: completedJob?.jobId || '',
|
|
5696
|
-
deviceIds,
|
|
5697
|
-
remoteDirectory: req.body?.remoteDirectory || ''
|
|
5698
|
-
}),
|
|
5699
|
-
authMethod: 'local-admin-session',
|
|
5700
|
-
details: {
|
|
5701
|
-
state: completedJob?.state || '',
|
|
5702
|
-
totalFiles: Number(completedJob?.totalFiles || 0),
|
|
5703
|
-
completedFiles: Number(completedJob?.completedFiles || 0),
|
|
5704
|
-
totalBytes: Number(completedJob?.totalBytes || 0),
|
|
5705
|
-
sentBytes: Number(completedJob?.sentBytes || 0),
|
|
5706
|
-
failedTargets: Array.isArray(completedJob?.failedTargets)
|
|
5707
|
-
? completedJob.failedTargets.length
|
|
5708
|
-
: 0
|
|
5709
|
-
}
|
|
5710
|
-
});
|
|
5711
|
-
}
|
|
5026
|
+
remoteDirectory: req.body?.remoteDirectory
|
|
5712
5027
|
});
|
|
5713
5028
|
res.status(202).json({ ok: true, jobId: job.jobId, state: job.state });
|
|
5714
5029
|
} catch (error) {
|
|
@@ -5790,80 +5105,56 @@ app.post('/api/remote/filesystem/shared-folders/:folderId/sync', requireHubFeatu
|
|
|
5790
5105
|
}
|
|
5791
5106
|
});
|
|
5792
5107
|
|
|
5793
|
-
app.post('/api/remote/files/transfer', requireHubFeatureAccess,
|
|
5108
|
+
app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
|
|
5794
5109
|
noStore(res);
|
|
5795
5110
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
|
|
5796
5111
|
if (deviceIds.length === 0) {
|
|
5797
|
-
recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
|
|
5798
5112
|
res.status(400).json({ ok: false, error: 'no-target-devices' });
|
|
5799
5113
|
return;
|
|
5800
5114
|
}
|
|
5801
5115
|
const normalized = normalizeTransferFiles(req.body?.files);
|
|
5802
5116
|
if (!normalized.ok) {
|
|
5803
|
-
recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
|
|
5804
5117
|
res.status(400).json({ ok: false, error: normalized.error, totalBytes: normalized.totalBytes || 0 });
|
|
5805
5118
|
return;
|
|
5806
5119
|
}
|
|
5807
5120
|
const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
|
|
5808
5121
|
const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
|
|
5809
5122
|
const queuedAt = new Date().toISOString();
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
deviceIds,
|
|
5835
|
-
sessionId: transferId,
|
|
5836
|
-
requestHash: requestAuditHash({ transferId, remoteDirectory, files: normalized.files.map(file => ({ name: file.name, relativePath: file.relativePath, byteLength: file.size })), totalBytes: normalized.totalBytes }),
|
|
5837
|
-
authMethod: 'local-admin-session',
|
|
5838
|
-
details: { queued, acknowledged, total: deviceIds.length, totalBytes: normalized.totalBytes, files: normalized.files.length }
|
|
5839
|
-
});
|
|
5840
|
-
res.json({
|
|
5841
|
-
ok: success,
|
|
5842
|
-
transferId,
|
|
5843
|
-
queued,
|
|
5844
|
-
acknowledged,
|
|
5845
|
-
total: deviceIds.length,
|
|
5846
|
-
totalBytes: normalized.totalBytes,
|
|
5847
|
-
files: normalized.files.length,
|
|
5848
|
-
results,
|
|
5849
|
-
error
|
|
5850
|
-
});
|
|
5851
|
-
} catch (error) {
|
|
5852
|
-
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5853
|
-
}
|
|
5123
|
+
const results = deviceIds.map(deviceId => ({
|
|
5124
|
+
deviceId,
|
|
5125
|
+
...remoteHub.sendCommand(deviceId, {
|
|
5126
|
+
command: 'file.transfer',
|
|
5127
|
+
payload: {
|
|
5128
|
+
transferId,
|
|
5129
|
+
remoteDirectory,
|
|
5130
|
+
files: normalized.files,
|
|
5131
|
+
totalBytes: normalized.totalBytes,
|
|
5132
|
+
requestedAt: queuedAt
|
|
5133
|
+
}
|
|
5134
|
+
})
|
|
5135
|
+
}));
|
|
5136
|
+
const queued = results.filter(result => result.ok).length;
|
|
5137
|
+
res.json({
|
|
5138
|
+
ok: queued > 0,
|
|
5139
|
+
transferId,
|
|
5140
|
+
queued,
|
|
5141
|
+
total: deviceIds.length,
|
|
5142
|
+
totalBytes: normalized.totalBytes,
|
|
5143
|
+
files: normalized.files.length,
|
|
5144
|
+
results,
|
|
5145
|
+
error: queued > 0 ? undefined : 'no-transfer-queued'
|
|
5146
|
+
});
|
|
5854
5147
|
});
|
|
5855
5148
|
|
|
5856
|
-
app.post('/api/remote/files/chunk', requireHubFeatureAccess,
|
|
5149
|
+
app.post('/api/remote/files/chunk', requireHubFeatureAccess, (req, res) => {
|
|
5857
5150
|
noStore(res);
|
|
5858
5151
|
const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
|
|
5859
5152
|
if (deviceIds.length === 0) {
|
|
5860
|
-
recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
|
|
5861
5153
|
res.status(400).json({ ok: false, error: 'no-target-devices' });
|
|
5862
5154
|
return;
|
|
5863
5155
|
}
|
|
5864
5156
|
const normalized = normalizeTransferChunk(req.body || {});
|
|
5865
5157
|
if (!normalized.ok) {
|
|
5866
|
-
recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
|
|
5867
5158
|
res.status(400).json({ ok: false, error: normalized.error, byteLength: normalized.byteLength || 0 });
|
|
5868
5159
|
return;
|
|
5869
5160
|
}
|
|
@@ -5871,50 +5162,30 @@ app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) =>
|
|
|
5871
5162
|
const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
|
|
5872
5163
|
const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
|
|
5873
5164
|
const queuedAt = new Date().toISOString();
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
sessionId: transferId,
|
|
5899
|
-
requestHash: requestAuditHash({ transferId, remoteDirectory, offset: normalized.chunk.offset, byteLength: normalized.chunk.byteLength, final: normalized.chunk.final }),
|
|
5900
|
-
authMethod: 'local-admin-session',
|
|
5901
|
-
details: { queued, acknowledged, total: deviceIds.length, byteLength: normalized.chunk.byteLength, offset: normalized.chunk.offset, final: normalized.chunk.final }
|
|
5902
|
-
});
|
|
5903
|
-
res.json({
|
|
5904
|
-
ok: success,
|
|
5905
|
-
transferId,
|
|
5906
|
-
queued,
|
|
5907
|
-
acknowledged,
|
|
5908
|
-
total: deviceIds.length,
|
|
5909
|
-
byteLength: normalized.chunk.byteLength,
|
|
5910
|
-
offset: normalized.chunk.offset,
|
|
5911
|
-
final: normalized.chunk.final,
|
|
5912
|
-
results,
|
|
5913
|
-
error
|
|
5914
|
-
});
|
|
5915
|
-
} catch (error) {
|
|
5916
|
-
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5917
|
-
}
|
|
5165
|
+
const results = deviceIds.map(deviceId => ({
|
|
5166
|
+
deviceId,
|
|
5167
|
+
...remoteHub.sendCommand(deviceId, {
|
|
5168
|
+
command: 'file.transfer.chunk',
|
|
5169
|
+
payload: {
|
|
5170
|
+
transferId,
|
|
5171
|
+
remoteDirectory,
|
|
5172
|
+
...normalized.chunk,
|
|
5173
|
+
requestedAt: queuedAt
|
|
5174
|
+
}
|
|
5175
|
+
})
|
|
5176
|
+
}));
|
|
5177
|
+
const queued = results.filter(result => result.ok).length;
|
|
5178
|
+
res.json({
|
|
5179
|
+
ok: queued > 0,
|
|
5180
|
+
transferId,
|
|
5181
|
+
queued,
|
|
5182
|
+
total: deviceIds.length,
|
|
5183
|
+
byteLength: normalized.chunk.byteLength,
|
|
5184
|
+
offset: normalized.chunk.offset,
|
|
5185
|
+
final: normalized.chunk.final,
|
|
5186
|
+
results,
|
|
5187
|
+
error: queued > 0 ? undefined : 'no-transfer-queued'
|
|
5188
|
+
});
|
|
5918
5189
|
});
|
|
5919
5190
|
|
|
5920
5191
|
const MANUAL_POWER_ACTIONS = new Set(['lock', 'sleep', 'restart', 'shutdown']);
|
|
@@ -6203,65 +5474,14 @@ if (existsSync(webIndexPath)) {
|
|
|
6203
5474
|
});
|
|
6204
5475
|
}
|
|
6205
5476
|
|
|
6206
|
-
function isAuthorizedAdminWebSocket(req) {
|
|
6207
|
-
if (!enforceLocalAdminAuth) return true;
|
|
6208
|
-
const protocols = String(req.headers['sec-websocket-protocol'] || '')
|
|
6209
|
-
.split(',')
|
|
6210
|
-
.map(value => value.trim())
|
|
6211
|
-
.filter(Boolean);
|
|
6212
|
-
const adminToken = protocols.find(value => value.startsWith('livedesk-admin-'))?.slice('livedesk-admin-'.length) || '';
|
|
6213
|
-
const csrfToken = protocols.find(value => value.startsWith('livedesk-admin-csrf-'))?.slice('livedesk-admin-csrf-'.length) || '';
|
|
6214
|
-
return secureExactTestTokenMatches(adminToken, localAdminSessionToken)
|
|
6215
|
-
&& secureExactTestTokenMatches(csrfToken, localAdminCsrfToken);
|
|
6216
|
-
}
|
|
6217
|
-
|
|
6218
5477
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
6219
5478
|
if (!isTrustedBrowserRequest(req)) {
|
|
6220
5479
|
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
6221
5480
|
socket.destroy();
|
|
6222
5481
|
return;
|
|
6223
5482
|
}
|
|
6224
|
-
if (!isAuthorizedAdminWebSocket(req)) {
|
|
6225
|
-
recordSecurityAudit({
|
|
6226
|
-
action: 'local-admin.websocket',
|
|
6227
|
-
phase: 'complete',
|
|
6228
|
-
result: 'rejected',
|
|
6229
|
-
reason: 'admin-session-required',
|
|
6230
|
-
requestHash: requestAuditHash({ path: req.url || '' }),
|
|
6231
|
-
authMethod: 'local-admin-websocket'
|
|
6232
|
-
});
|
|
6233
|
-
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
6234
|
-
socket.destroy();
|
|
6235
|
-
return;
|
|
6236
|
-
}
|
|
6237
|
-
let parsed;
|
|
6238
5483
|
try {
|
|
6239
|
-
parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
6240
|
-
} catch {
|
|
6241
|
-
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
6242
|
-
socket.destroy();
|
|
6243
|
-
return;
|
|
6244
|
-
}
|
|
6245
|
-
const websocketPath = parsed.pathname;
|
|
6246
|
-
if (![
|
|
6247
|
-
'/api/remote/frames/ws',
|
|
6248
|
-
'/api/remote/atlas/ws',
|
|
6249
|
-
'/api/remote/input/ws',
|
|
6250
|
-
'/api/remote/audio/ws'
|
|
6251
|
-
].includes(websocketPath)) {
|
|
6252
|
-
socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
|
|
6253
|
-
socket.destroy();
|
|
6254
|
-
return;
|
|
6255
|
-
}
|
|
6256
|
-
void recordSecurityAuditRequired({
|
|
6257
|
-
action: 'local-admin.websocket',
|
|
6258
|
-
phase: 'accepted',
|
|
6259
|
-
result: 'accepted',
|
|
6260
|
-
sessionId: localAdminSessionId,
|
|
6261
|
-
requestHash: requestAuditHash({ path: websocketPath }),
|
|
6262
|
-
authMethod: 'local-admin-websocket',
|
|
6263
|
-
details: { path: websocketPath }
|
|
6264
|
-
}).then(() => {
|
|
5484
|
+
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
6265
5485
|
if (parsed.pathname === '/api/remote/frames/ws') {
|
|
6266
5486
|
frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
|
|
6267
5487
|
return;
|
|
@@ -6278,10 +5498,12 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
6278
5498
|
audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
|
|
6279
5499
|
return;
|
|
6280
5500
|
}
|
|
6281
|
-
|
|
6282
|
-
socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
|
5501
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
6283
5502
|
socket.destroy();
|
|
6284
|
-
}
|
|
5503
|
+
} catch {
|
|
5504
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
5505
|
+
socket.destroy();
|
|
5506
|
+
}
|
|
6285
5507
|
});
|
|
6286
5508
|
|
|
6287
5509
|
frameWss.on('connection', (ws, req) => {
|
|
@@ -6305,11 +5527,11 @@ frameWss.on('connection', (ws, req) => {
|
|
|
6305
5527
|
ws.on('message', data => {
|
|
6306
5528
|
try {
|
|
6307
5529
|
const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
|
|
6308
|
-
if (payload?.type === 'subscribe') {
|
|
6309
|
-
updateFrameSubscription(ws, payload);
|
|
6310
|
-
} else if (payload?.type === 'refresh-live') {
|
|
6311
|
-
refreshFrameSubscriptionLive(ws, payload);
|
|
6312
|
-
} else if (payload?.type === 'restart-live') {
|
|
5530
|
+
if (payload?.type === 'subscribe') {
|
|
5531
|
+
updateFrameSubscription(ws, payload);
|
|
5532
|
+
} else if (payload?.type === 'refresh-live') {
|
|
5533
|
+
refreshFrameSubscriptionLive(ws, payload);
|
|
5534
|
+
} else if (payload?.type === 'restart-live') {
|
|
6313
5535
|
restartFrameSubscriptionLive(ws, payload);
|
|
6314
5536
|
}
|
|
6315
5537
|
} catch {
|
|
@@ -6398,17 +5620,12 @@ inputWss.on('connection', ws => {
|
|
|
6398
5620
|
inputClients.add(ws);
|
|
6399
5621
|
ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
|
|
6400
5622
|
ws.liveDeskInputDeviceIds = new Set();
|
|
6401
|
-
ws.liveDeskInputAuditReady = false;
|
|
6402
5623
|
try {
|
|
6403
5624
|
ws._socket?.setNoDelay?.(true);
|
|
6404
5625
|
} catch {
|
|
6405
5626
|
// Best-effort latency hint for browser input sockets.
|
|
6406
5627
|
}
|
|
6407
5628
|
ws.on('message', data => {
|
|
6408
|
-
if (ws.liveDeskInputAuditReady !== true) {
|
|
6409
|
-
sendJson(ws, { type: 'RemoteInputError', error: 'security-audit-pending' });
|
|
6410
|
-
return;
|
|
6411
|
-
}
|
|
6412
5629
|
let payload;
|
|
6413
5630
|
try {
|
|
6414
5631
|
payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
|
|
@@ -6449,43 +5666,19 @@ inputWss.on('connection', ws => {
|
|
|
6449
5666
|
});
|
|
6450
5667
|
});
|
|
6451
5668
|
const releaseBrowserInputOwner = reason => {
|
|
6452
|
-
if (ws.liveDeskInputReleased) return;
|
|
6453
|
-
ws.liveDeskInputReleased = true;
|
|
6454
5669
|
inputClients.delete(ws);
|
|
6455
5670
|
for (const deviceId of ws.liveDeskInputDeviceIds || []) {
|
|
6456
5671
|
remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
|
|
6457
5672
|
}
|
|
6458
|
-
void recordSecurityAuditRequired({
|
|
6459
|
-
action: 'remote.control.browser-session',
|
|
6460
|
-
phase: 'complete',
|
|
6461
|
-
result: 'success',
|
|
6462
|
-
reason,
|
|
6463
|
-
deviceIds: [...(ws.liveDeskInputDeviceIds || [])],
|
|
6464
|
-
sessionId: ws.liveDeskInputClientId,
|
|
6465
|
-
authMethod: 'local-admin-websocket'
|
|
6466
|
-
}).catch(() => {
|
|
6467
|
-
// The global fail-closed gate is marked by recordSecurityAuditRequired.
|
|
6468
|
-
});
|
|
6469
5673
|
ws.liveDeskInputDeviceIds?.clear?.();
|
|
6470
5674
|
};
|
|
6471
5675
|
ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
|
|
6472
5676
|
ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
authMethod: 'local-admin-websocket'
|
|
6479
|
-
}).then(() => {
|
|
6480
|
-
ws.liveDeskInputAuditReady = true;
|
|
6481
|
-
sendJson(ws, {
|
|
6482
|
-
type: 'RemoteInputSocketReady',
|
|
6483
|
-
protocol: 'livedesk.remote.input.json.v1',
|
|
6484
|
-
clientId: ws.liveDeskInputClientId,
|
|
6485
|
-
timestamp: new Date().toISOString()
|
|
6486
|
-
});
|
|
6487
|
-
}).catch(() => {
|
|
6488
|
-
ws.close(1011, 'security-audit-unavailable');
|
|
5677
|
+
sendJson(ws, {
|
|
5678
|
+
type: 'RemoteInputSocketReady',
|
|
5679
|
+
protocol: 'livedesk.remote.input.json.v1',
|
|
5680
|
+
clientId: ws.liveDeskInputClientId,
|
|
5681
|
+
timestamp: new Date().toISOString()
|
|
6489
5682
|
});
|
|
6490
5683
|
});
|
|
6491
5684
|
|
|
@@ -6501,9 +5694,6 @@ httpServer.listen(httpPort, httpHost, () => {
|
|
|
6501
5694
|
console.log(`[LiveDesk Hub] Version ${managerVersion}`);
|
|
6502
5695
|
console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
|
|
6503
5696
|
console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
|
|
6504
|
-
void restorePersistedHubSessionAtStartup().catch(error => {
|
|
6505
|
-
console.warn(`[LiveDesk Hub] Saved sign-in startup restore deferred to the Hub page: ${error?.message || error}`);
|
|
6506
|
-
});
|
|
6507
5697
|
});
|
|
6508
5698
|
|
|
6509
5699
|
const roleWatchTimer = runtimeRole === 'hub'
|
|
@@ -6585,7 +5775,6 @@ function shutdownHub(signal) {
|
|
|
6585
5775
|
const startedAt = Date.now();
|
|
6586
5776
|
console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
|
|
6587
5777
|
if (roleWatchTimer) clearInterval(roleWatchTimer);
|
|
6588
|
-
clearInterval(captureRetentionTimer);
|
|
6589
5778
|
clearInterval(browserWebSocketHeartbeatTimer);
|
|
6590
5779
|
runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
|
|
6591
5780
|
atlasClients.clear();
|