@livedesk/hub 0.1.35 → 0.1.37

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/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 { createSecurityAuditStore } from './security/security-audit-store.js';
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
- if (!session || typeof session !== 'object') return null;
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
- if (!runtimeRefreshSecretStore.write(normalized.session.refresh_token)) return false;
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,107 +506,46 @@ async function getRuntimeAccessToken() {
589
506
  return accessToken;
590
507
  }
591
508
 
592
- if (runtimeRefreshPromise) return runtimeRefreshPromise;
593
-
594
- const ownerGeneration = runtimeSessionGeneration;
595
- const refreshOperation = (async () => {
596
- const response = await fetchAuthResponse(
597
- fetch,
598
- `${supabaseUrl}/auth/v1/token?grant_type=refresh_token`,
599
- {
600
- method: 'POST',
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
- SUPABASE_AUTH_TIMEOUT_MS
609
- );
610
- if (!response.ok) {
611
- if (response.status === 400 || response.status === 401 || response.status === 403) {
612
- if (runtimeSessionGeneration === ownerGeneration && runtimeRefreshToken === refreshToken) {
613
- clearRuntimeSession();
614
- }
615
- throw new Error(`hub-session-refresh-failed:${response.status}`);
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
- return runtimeAccessToken;
641
- })();
642
- runtimeRefreshPromise = refreshOperation;
643
- try {
644
- return await refreshOperation;
645
- } finally {
646
- if (runtimeRefreshPromise === refreshOperation) runtimeRefreshPromise = null;
528
+ throw new Error(`hub-session-refresh-provider-failed:${response.status}`);
647
529
  }
648
- }
649
-
650
- function isLoopbackBindHost(value) {
651
- const host = String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
652
- return host === '127.0.0.1' || host === 'localhost' || host === '::1';
653
- }
654
-
655
- async function revokeRuntimeProviderSession() {
656
- const hasRuntimeSession = Boolean(runtimeAccessToken || runtimeRefreshToken);
657
- if (!hasRuntimeSession) return { ok: true, skipped: true, reason: 'no-runtime-session' };
658
-
659
- let accessToken = String(runtimeAccessToken || '').trim();
660
- try {
661
- accessToken = await getRuntimeAccessToken() || accessToken;
662
- } catch (error) {
663
- const message = String(error?.message || error);
664
- if (/^hub-session-refresh-failed:(?:400|401|403)$/.test(message)) {
665
- return { ok: true, alreadyInvalid: true };
666
- }
667
- 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');
668
534
  }
669
- if (!accessToken) return { ok: false, error: 'provider-session-token-unavailable' };
670
-
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);
671
539
  try {
672
- const response = await fetchAuthResponse(
673
- fetch,
674
- `${supabaseUrl}/auth/v1/logout?scope=local`,
675
- {
676
- method: 'POST',
677
- headers: {
678
- apikey: supabasePublishableKey,
679
- Authorization: `Bearer ${accessToken}`,
680
- Accept: 'application/json'
681
- }
682
- },
683
- SUPABASE_AUTH_TIMEOUT_MS
684
- );
685
- if (response.ok) return { ok: true, revoked: true };
686
- if ([401, 403, 404].includes(response.status)) {
687
- return { ok: true, alreadyInvalid: true, status: response.status };
688
- }
689
- 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
+ });
690
545
  } catch (error) {
691
- return { ok: false, error: String(error?.message || error) };
546
+ console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
692
547
  }
548
+ return runtimeAccessToken;
693
549
  }
694
550
 
695
551
  async function verifySupabaseUser(accessToken) {
@@ -804,80 +660,14 @@ function requireHubFeatureAccess(_req, res, next) {
804
660
  }
805
661
 
806
662
  const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
807
- securityAuditStore = createSecurityAuditStore({ dataDir: agentDataDir });
808
-
809
- function requestAuditHash(value) {
810
- try {
811
- return crypto.createHash('sha256').update(JSON.stringify(value ?? {}), 'utf8').digest('base64url');
812
- } catch {
813
- return crypto.createHash('sha256').update('[unserializable]', 'utf8').digest('base64url');
814
- }
815
- }
816
-
817
- function securityAuditEvent(event = {}) {
818
- const runtime = runtimeManager.getSnapshot();
819
- return {
820
- actorAccountId: event.actorAccountId || runtime.userId || '',
821
- actorUserId: event.actorUserId || runtime.userId || '',
822
- hubId: event.hubId || runtime.deviceId || '',
823
- ...event
824
- };
825
- }
826
-
827
- function markSecurityAuditFailed(error) {
828
- if (securityAuditHealthy) {
829
- securityAuditHealthy = false;
830
- console.error(`[LiveDesk Hub] SECURITY AUDIT FAILURE: ${error?.message || error}`);
831
- }
832
- }
833
-
834
- async function recordSecurityAuditRequired(event = {}) {
835
- if (!securityAuditStore || !securityAuditHealthy) {
836
- const error = new Error('security-audit-unavailable');
837
- error.code = 'security-audit-unavailable';
838
- throw error;
839
- }
840
- try {
841
- return await securityAuditStore.record(securityAuditEvent(event));
842
- } catch (error) {
843
- markSecurityAuditFailed(error);
844
- throw error;
845
- }
846
- }
847
-
848
- function recordSecurityAudit(event = {}) {
849
- void recordSecurityAuditRequired(event).catch(() => {
850
- // Required mutation paths await recordSecurityAuditRequired directly.
851
- // Best-effort lifecycle telemetry still marks the global audit gate failed.
852
- });
853
- }
854
-
855
663
  const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
856
- const captureStore = new CaptureStore({
857
- dataDir: agentDataDir,
858
- getRetentionPolicy: () => liveDeskSettingsStore.getCached()?.filesAudio?.captureAutoDelete || 'never',
859
- onRetentionDelete: result => recordSecurityAudit({
860
- action: 'privacy.capture.retention-delete',
861
- phase: 'complete',
862
- result: 'success',
863
- authMethod: 'settings-retention-policy',
864
- details: result
865
- })
866
- });
664
+ const captureStore = new CaptureStore({ dataDir: agentDataDir });
867
665
  void captureStore.initialize().catch(error => {
868
666
  console.warn(`[LiveDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
869
667
  });
870
- void liveDeskSettingsStore.getRecord()
871
- .then(() => captureStore.enforceRetention())
872
- .catch(error => {
873
- console.warn(`[LiveDesk Hub] settings or capture retention load failed: ${error instanceof Error ? error.message : String(error)}`);
874
- });
875
- const captureRetentionTimer = setInterval(() => {
876
- void captureStore.enforceRetention().catch(error => {
877
- console.warn(`[LiveDesk Hub] capture retention failed: ${error instanceof Error ? error.message : String(error)}`);
878
- });
879
- }, 60 * 60_000);
880
- captureRetentionTimer.unref?.();
668
+ void liveDeskSettingsStore.getRecord().catch(error => {
669
+ console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
670
+ });
881
671
 
882
672
  const udpTransport = createHubUdpTransport({
883
673
  env: process.env,
@@ -888,7 +678,6 @@ const udpTransport = createHubUdpTransport({
888
678
  const remoteHub = createRemoteHub({
889
679
  managerPackage: '@livedesk/hub',
890
680
  managerVersion: packageInfo.version,
891
- dataDir: agentDataDir,
892
681
  env: {
893
682
  ...process.env,
894
683
  MINDEXEC_MANAGER_PACKAGE: '@livedesk/hub',
@@ -900,10 +689,6 @@ const remoteHub = createRemoteHub({
900
689
  emitEvent: handleRemoteHubEvent,
901
690
  emitFrame: broadcastRemoteBinaryFrame,
902
691
  emitAudio: broadcastRemoteBinaryAudio,
903
- getSecurityIdentity: () => ({
904
- accountId: runtimeManager.getSnapshot().userId || testSecurityAccountId,
905
- hubId: remoteHub?.getSecurityStatus?.().hubId || ''
906
- }),
907
692
  getEffectiveDevicePolicy: ({ deviceId, capabilities }) => buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities }),
908
693
  getWelcomeDevicePolicy: ({ deviceId, capabilities }) => {
909
694
  const policy = buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities });
@@ -1008,8 +793,6 @@ if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
1008
793
  remoteHub,
1009
794
  currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
1010
795
  currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
1011
- updateManifestUrl: process.env.LIVEDESK_UPDATE_MANIFEST_URL || '',
1012
- updatePublicKey: process.env.LIVEDESK_UPDATE_PUBLIC_KEY || '',
1013
796
  restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
1014
797
  requestHubRestart
1015
798
  });
@@ -1337,21 +1120,6 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
1337
1120
  return { ok: false, error: 'codex-tool-limit-reached' };
1338
1121
  }
1339
1122
  session.toolCallCount += 1;
1340
- if (isRetiredAgentMutatingToolName(name)) {
1341
- recordAgentAudit({
1342
- event: 'tool-rejected',
1343
- runId: session.runId,
1344
- deviceIds: [],
1345
- toolName: name,
1346
- category: 'mutation',
1347
- decision: 'deny',
1348
- status: 'rejected',
1349
- permissionMode: session.permissionPolicy?.mode,
1350
- policyHash: session.permissionPolicyHash,
1351
- details: { reason: 'agent-mutating-tool-disabled' }
1352
- });
1353
- return { ok: false, error: 'agent-mutating-tool-disabled' };
1354
- }
1355
1123
  const tool = getAgentToolDefinition(name);
1356
1124
  if (!tool) return { ok: false, error: 'codex-tool-not-allowed' };
1357
1125
  const validationError = validateAgentMcpArguments(name, args);
@@ -1544,18 +1312,11 @@ async function synchronizeAgentEnablement() {
1544
1312
  }
1545
1313
 
1546
1314
  const hubFilesystem = createHubFilesystem();
1547
- const fileTransferCommandAckTimeoutMs = readPositiveIntegerEnv(
1548
- 'LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS',
1549
- 30_000
1550
- );
1551
1315
  hubTransferJobs = createHubTransferJobs({
1552
1316
  filesystem: hubFilesystem,
1553
1317
  remoteHub,
1554
1318
  maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
1555
- commandResultTimeoutMs: fileTransferCommandAckTimeoutMs,
1556
- getMaxFileSizeBytes: () => Number(
1557
- liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
1558
- || 1024 * 1024 * 1024)
1319
+ commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
1559
1320
  });
1560
1321
  const hubSharedFolders = createHubSharedFolders({
1561
1322
  filesystem: hubFilesystem,
@@ -1676,7 +1437,7 @@ app.use((req, res, next) => {
1676
1437
  res.setHeader('Access-Control-Allow-Origin', origin);
1677
1438
  res.setHeader('Access-Control-Allow-Private-Network', 'true');
1678
1439
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
1679
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-LiveDesk-Admin-Session, X-LiveDesk-Admin-CSRF, X-LiveDesk-CSRF');
1440
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
1680
1441
  res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
1681
1442
  }
1682
1443
  if (req.method === 'OPTIONS') {
@@ -1685,143 +1446,7 @@ app.use((req, res, next) => {
1685
1446
  }
1686
1447
  next();
1687
1448
  });
1688
- const parseHubJsonBody = express.json({ limit: '32mb' });
1689
- function parseHubJsonRequest(req, res, next, onParsed = next) {
1690
- parseHubJsonBody(req, res, error => {
1691
- if (!error) {
1692
- onParsed();
1693
- return;
1694
- }
1695
- const type = String(error?.type || '');
1696
- const code = String(error?.code || '');
1697
- const bodyError = type === 'entity.parse.failed'
1698
- ? { status: 400, error: 'invalid-json' }
1699
- : type === 'entity.too.large'
1700
- ? { status: 413, error: 'request-too-large' }
1701
- : type === 'request.aborted' || type === 'stream.not.readable' || code === 'ECONNABORTED'
1702
- ? { status: 400, error: 'request-body-unavailable' }
1703
- : null;
1704
- if (!bodyError) {
1705
- next(error);
1706
- return;
1707
- }
1708
- if (res.headersSent || res.destroyed || req.destroyed) return;
1709
- res.status(bodyError.status).json({ ok: false, error: bodyError.error });
1710
- });
1711
- }
1712
- app.use((req, res, next) => {
1713
- const mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
1714
- if (!enforceLocalAdminAuth || req.path === '/api/security/session' || req.method === 'OPTIONS') {
1715
- if (mutating) parseHubJsonRequest(req, res, next);
1716
- else next();
1717
- return;
1718
- }
1719
- const authorizedLauncherShutdown = req.method === 'POST'
1720
- && req.path === '/api/runtime/shutdown'
1721
- && isLoopbackAddress(req.socket?.remoteAddress)
1722
- && launcherShutdownToken
1723
- && secureExactTestTokenMatches(
1724
- String(req.headers['x-livedesk-launcher-shutdown'] || ''),
1725
- launcherShutdownToken
1726
- );
1727
- if (authorizedLauncherShutdown) {
1728
- req.liveDeskLauncherShutdown = true;
1729
- parseHubJsonRequest(req, res, next);
1730
- return;
1731
- }
1732
- const sensitiveRead = req.method === 'GET'
1733
- && ['/api/security/audit', '/api/security/rendezvous-issuer', '/api/privacy/inventory'].includes(req.path);
1734
- if (!mutating && !sensitiveRead) {
1735
- next();
1736
- return;
1737
- }
1738
- const adminToken = String(req.headers['x-livedesk-admin-session'] || '');
1739
- const csrfToken = String(req.headers['x-livedesk-admin-csrf'] || '');
1740
- if (!secureExactTestTokenMatches(adminToken, localAdminSessionToken)) {
1741
- recordSecurityAudit({
1742
- action: 'local-admin.request',
1743
- phase: 'complete',
1744
- result: 'rejected',
1745
- reason: 'admin-session-required',
1746
- requestHash: requestAuditHash({ method: req.method, path: req.path }),
1747
- authMethod: 'local-admin-session'
1748
- });
1749
- res.status(401).json({ ok: false, error: 'local-admin-session-required' });
1750
- return;
1751
- }
1752
- if (mutating && !secureExactTestTokenMatches(csrfToken, localAdminCsrfToken)) {
1753
- recordSecurityAudit({
1754
- action: 'local-admin.request',
1755
- phase: 'complete',
1756
- result: 'rejected',
1757
- reason: 'csrf-token-required',
1758
- requestHash: requestAuditHash({ method: req.method, path: req.path }),
1759
- authMethod: 'local-admin-session'
1760
- });
1761
- res.status(403).json({ ok: false, error: 'csrf-token-required' });
1762
- return;
1763
- }
1764
- if (!securityAuditHealthy) {
1765
- res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
1766
- return;
1767
- }
1768
- req.liveDeskAdminSessionId = localAdminSessionId;
1769
- if (!mutating) {
1770
- next();
1771
- return;
1772
- }
1773
-
1774
- const beginMutationAudit = () => {
1775
- const mutationRequestHash = requestAuditHash({ method: req.method, path: req.path });
1776
- void recordSecurityAuditRequired({
1777
- action: 'local-admin.mutation',
1778
- phase: 'accepted',
1779
- result: 'accepted',
1780
- sessionId: localAdminSessionId,
1781
- requestHash: mutationRequestHash,
1782
- authMethod: 'local-admin-session',
1783
- details: { method: req.method, path: req.path }
1784
- }).then(() => {
1785
- const originalEnd = res.end.bind(res);
1786
- let completionStarted = false;
1787
- res.end = (...args) => {
1788
- if (completionStarted) return res;
1789
- completionStarted = true;
1790
- const statusCode = Number(res.statusCode || 200);
1791
- void recordSecurityAuditRequired({
1792
- action: 'local-admin.mutation',
1793
- phase: 'complete',
1794
- result: statusCode >= 200 && statusCode < 400 ? 'success' : 'rejected',
1795
- reason: statusCode >= 400 ? `http-${statusCode}` : '',
1796
- sessionId: localAdminSessionId,
1797
- requestHash: mutationRequestHash,
1798
- authMethod: 'local-admin-session',
1799
- details: { method: req.method, path: req.path, statusCode }
1800
- }).then(() => {
1801
- originalEnd(...args);
1802
- }).catch(error => {
1803
- if (res.headersSent) {
1804
- res.destroy(error);
1805
- return;
1806
- }
1807
- const callback = [...args].reverse().find(value => typeof value === 'function');
1808
- const body = JSON.stringify({ ok: false, error: 'security-audit-unavailable' });
1809
- res.statusCode = 503;
1810
- res.removeHeader('Content-Length');
1811
- res.removeHeader('Transfer-Encoding');
1812
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
1813
- res.setHeader('Content-Length', Buffer.byteLength(body));
1814
- originalEnd(body, 'utf8', callback);
1815
- });
1816
- return res;
1817
- };
1818
- next();
1819
- }).catch(() => {
1820
- res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
1821
- });
1822
- };
1823
- parseHubJsonRequest(req, res, next, beginMutationAudit);
1824
- });
1449
+ app.use(express.json({ limit: '32mb' }));
1825
1450
  app.use((req, res, next) => {
1826
1451
  if (runtimeRole === 'client' && req.path.startsWith('/api/remote')) {
1827
1452
  res.status(403).json(roleGuardResponse(runtimeRole, 'hub'));
@@ -1976,7 +1601,6 @@ function normalizeTransferFiles(value) {
1976
1601
  name,
1977
1602
  relativePath,
1978
1603
  size: byteLength,
1979
- sha256: crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex'),
1980
1604
  mimeType: String(entry?.type || entry?.mimeType || '').slice(0, 160),
1981
1605
  lastModified: Number(entry?.lastModified || 0) || 0,
1982
1606
  dataBase64
@@ -1996,31 +1620,16 @@ function normalizeTransferChunk(body = {}) {
1996
1620
  const totalBytes = Math.max(0, Math.floor(Number(body.totalBytes) || 0));
1997
1621
  const byteLength = dataBase64 ? Buffer.byteLength(dataBase64, 'base64') : 0;
1998
1622
  const final = body.final === true;
1999
- const maxFileSizeBytes = Math.max(1, Number(
2000
- liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
2001
- || 1024 * 1024 * 1024));
2002
1623
 
2003
1624
  if (!name || !relativePath || offset > totalBytes || offset + byteLength > totalBytes) {
2004
1625
  return { ok: false, error: 'invalid-file-transfer-chunk' };
2005
1626
  }
2006
- if (totalBytes > maxFileSizeBytes) {
2007
- return { ok: false, error: 'file-transfer-file-too-large', totalBytes };
2008
- }
2009
1627
  if (byteLength > MAX_FILE_TRANSFER_CHUNK_BYTES) {
2010
1628
  return { ok: false, error: 'file-transfer-chunk-too-large', byteLength };
2011
1629
  }
2012
1630
  if (byteLength === 0 && !(final && totalBytes === 0 && offset === 0)) {
2013
1631
  return { ok: false, error: 'empty-file-transfer-chunk' };
2014
1632
  }
2015
- const suppliedSha256 = String(body.sha256 || '').trim().toLowerCase();
2016
- const sha256 = /^[a-f0-9]{64}$/.test(suppliedSha256)
2017
- ? suppliedSha256
2018
- : final && offset === 0 && totalBytes === byteLength
2019
- ? crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex')
2020
- : '';
2021
- if (final && !sha256) {
2022
- return { ok: false, error: 'file-transfer-sha256-required', byteLength };
2023
- }
2024
1633
 
2025
1634
  return {
2026
1635
  ok: true,
@@ -2032,7 +1641,6 @@ function normalizeTransferChunk(body = {}) {
2032
1641
  byteLength,
2033
1642
  dataBase64,
2034
1643
  final,
2035
- sha256,
2036
1644
  mimeType: String(body.type || body.mimeType || '').slice(0, 160),
2037
1645
  lastModified: Number(body.lastModified || 0) || 0
2038
1646
  }
@@ -2738,11 +2346,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
2738
2346
  && bindingIdentity === frameLaneBindingIdentity(currentExpectedBinding)) {
2739
2347
  expectedBinding.readySent = currentExpectedBinding?.readySent === true
2740
2348
  || expectedBinding.readySent === true;
2741
- expectedBinding.replayedKeyFrameSeq = Math.max(
2742
- 0,
2743
- Number(currentExpectedBinding?.replayedKeyFrameSeq || 0),
2744
- Number(expectedBinding.replayedKeyFrameSeq || 0)
2745
- );
2746
2349
  ws.liveDeskExpectedStreamBindingsByDeviceId.set(normalizedDeviceId, expectedBinding);
2747
2350
  if (!lane.bindingEpochsByDeviceId.has(normalizedDeviceId)) {
2748
2351
  lane.bindingEpochsByDeviceId.set(normalizedDeviceId, 1);
@@ -2781,50 +2384,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
2781
2384
  return bindingIdentity;
2782
2385
  }
2783
2386
 
2784
- function replayExpectedBindingKeyFrame(ws, expectedBinding) {
2785
- if (!ws || ws.readyState !== ws.OPEN || !expectedBinding) {
2786
- return false;
2787
- }
2788
- const deviceId = String(expectedBinding.deviceId || '').trim();
2789
- const currentBinding = ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
2790
- ? ws.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
2791
- : null;
2792
- const bindingIdentity = frameLaneBindingIdentity(expectedBinding);
2793
- if (!deviceId
2794
- || !bindingIdentity
2795
- || bindingIdentity !== frameLaneBindingIdentity(currentBinding)) {
2796
- return false;
2797
- }
2798
- const cached = remoteHub.getFramePayload(deviceId, 'live', {
2799
- requireKeyFrame: true,
2800
- streamId: expectedBinding.streamId,
2801
- sessionId: expectedBinding.sessionId,
2802
- commandId: expectedBinding.commandId,
2803
- captureGeneration: expectedBinding.captureGeneration,
2804
- streamPurpose: expectedBinding.streamPurpose,
2805
- monitorIndex: expectedBinding.monitorIndex
2806
- });
2807
- const frameSeq = Number(cached?.frame?.frameSeq || 0);
2808
- if (!cached
2809
- || cached.frame?.isKeyFrame !== true
2810
- || !Number.isSafeInteger(frameSeq)
2811
- || frameSeq <= 0
2812
- || Number(currentBinding?.replayedKeyFrameSeq || 0) === frameSeq) {
2813
- return false;
2814
- }
2815
- currentBinding.replayedKeyFrameSeq = frameSeq;
2816
- broadcastRemoteBinaryFrame({
2817
- kind: 'live',
2818
- deviceId,
2819
- frame: cached.frame,
2820
- payload: cached.payload,
2821
- mimeType: cached.mimeType,
2822
- byteLength: cached.byteLength,
2823
- replayedForBinding: true
2824
- });
2825
- return true;
2826
- }
2827
-
2828
2387
  function dropQueuedFrameAtForLane(lane, dropIndex) {
2829
2388
  const droppedBefore = lane.dropped;
2830
2389
  const droppedItem = removeFrameLaneQueueItem(lane, dropIndex);
@@ -3187,37 +2746,6 @@ function updateFrameSubscription(ws, payload = {}) {
3187
2746
  }
3188
2747
  }
3189
2748
 
3190
- function refreshFrameSubscriptionLive(ws, payload = {}) {
3191
- if (!ws || ws.readyState !== 1) {
3192
- return;
3193
- }
3194
- const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
3195
- const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
3196
- const targetIds = requestedIds.length > 0
3197
- ? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
3198
- : subscribedIds;
3199
- if (targetIds.length === 0) {
3200
- return;
3201
- }
3202
- ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
3203
- payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
3204
- ));
3205
- ws.liveDeskLiveOptions = normalizeLiveOptions({
3206
- ...(ws.liveDeskLiveOptions || {}),
3207
- ...payload,
3208
- monitorSelections: {
3209
- ...(ws.liveDeskLiveOptions?.monitorSelections || {}),
3210
- ...(payload.monitorSelections || {})
3211
- }
3212
- });
3213
- for (const deviceId of targetIds.slice(0, 80)) {
3214
- startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
3215
- ...ws.liveDeskLiveOptions,
3216
- reuseExisting: true
3217
- });
3218
- }
3219
- }
3220
-
3221
2749
  function startFrameSubscriptionLive(
3222
2750
  ws,
3223
2751
  reason = 'subscribe',
@@ -3318,7 +2846,6 @@ function startFrameSubscriptionLive(
3318
2846
  continue;
3319
2847
  }
3320
2848
  cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
3321
- const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
3322
2849
  started.push({
3323
2850
  deviceId: expectedBinding.deviceId,
3324
2851
  sessionId: expectedBinding.sessionId,
@@ -3327,8 +2854,7 @@ function startFrameSubscriptionLive(
3327
2854
  commandId: expectedBinding.commandId,
3328
2855
  captureGeneration: expectedBinding.captureGeneration,
3329
2856
  monitorIndex: expectedBinding.monitorIndex,
3330
- ready: expectedBinding.readySent === true,
3331
- replayedKeyFrame,
2857
+ ready: reusedFrameReady,
3332
2858
  fps: result.fps,
3333
2859
  reused: result.reused === true
3334
2860
  });
@@ -4156,22 +3682,6 @@ app.get('/api/health', (_req, res) => {
4156
3682
  res.json(buildHubHealthPayload());
4157
3683
  });
4158
3684
 
4159
- app.get('/api/security/session', (req, res) => {
4160
- noStore(res);
4161
- if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostname(requestHostname(req))) {
4162
- res.status(403).json({ ok: false, error: 'local-admin-session-loopback-only' });
4163
- return;
4164
- }
4165
- res.json({
4166
- ok: true,
4167
- sessionId: localAdminSessionId,
4168
- adminToken: localAdminSessionToken,
4169
- csrfToken: localAdminCsrfToken,
4170
- issuedAt: new Date().toISOString(),
4171
- expiresOnProcessExit: true
4172
- });
4173
- });
4174
-
4175
3685
  if (persistentSessionGcEnabled) {
4176
3686
  app.post('/api/test/persistent-session/gc', (req, res) => {
4177
3687
  noStore(res);
@@ -4225,15 +3735,6 @@ app.patch('/api/settings', async (req, res) => {
4225
3735
  await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
4226
3736
  }
4227
3737
  remoteHub.refreshDevicePolicies();
4228
- void captureStore.enforceRetention().catch(() => undefined);
4229
- recordSecurityAudit({
4230
- action: 'settings.security-policy.updated',
4231
- phase: 'complete',
4232
- result: 'success',
4233
- requestHash: requestAuditHash(patch),
4234
- authMethod: 'local-admin-session',
4235
- details: { revision: record.revision, sections: Object.keys(patch) }
4236
- });
4237
3738
  res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
4238
3739
  } catch (error) {
4239
3740
  if (error instanceof SettingsConflictError) {
@@ -4404,124 +3905,10 @@ app.get('/api/security/trusted-devices', (_req, res) => {
4404
3905
  res.json({ ok: true, devices });
4405
3906
  });
4406
3907
 
4407
- app.get('/api/security/rendezvous-issuer', (_req, res) => {
4408
- noStore(res);
4409
- const security = remoteHub.getSecurityStatus();
4410
- res.json({
4411
- ok: true,
4412
- hubId: security.hubId,
4413
- issuerKeyId: security.hubIssuerKeyId,
4414
- publicKey: security.hubPublicKey,
4415
- algorithm: 'P-256/SHA-256',
4416
- environmentVariable: 'LIVEDESK_UDP_RENDEZVOUS_TRUSTED_ISSUER_PUBLIC_KEYS'
4417
- });
4418
- });
4419
-
4420
- app.get('/api/security/audit', async (req, res) => {
4421
- noStore(res);
4422
- try {
4423
- res.json({
4424
- ok: true,
4425
- healthy: securityAuditHealthy,
4426
- verification: await securityAuditStore.verify(),
4427
- audit: await securityAuditStore.list({ limit: req.query?.limit, action: req.query?.action })
4428
- });
4429
- } catch (error) {
4430
- securityAuditHealthy = false;
4431
- res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
4432
- }
4433
- });
4434
-
4435
- app.get('/api/privacy/inventory', async (_req, res) => {
4436
- noStore(res);
4437
- try {
4438
- const [captures, sharedFolders, verification] = await Promise.all([
4439
- captureStore.list(),
4440
- hubSharedFolders.list(),
4441
- securityAuditStore.verify()
4442
- ]);
4443
- res.json({
4444
- ok: true,
4445
- inventory: {
4446
- authenticated: runtimeManager.getSnapshot().authenticated === true,
4447
- captures: captures.length,
4448
- sharedFolderDefinitions: sharedFolders.length,
4449
- trustedDeviceCredentials: remoteHub.getSecurityStatus().devices.length,
4450
- securityAuditRecords: verification.records,
4451
- agentAuditRetained: (await agentAuditStore.list({ limit: 500 })).length
4452
- }
4453
- });
4454
- } catch (error) {
4455
- res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-inventory-unavailable').slice(0, 120) });
4456
- }
4457
- });
4458
-
4459
- app.delete('/api/privacy/local-data', async (req, res) => {
4460
- noStore(res);
4461
- if (String(req.body?.confirmation || '') !== 'DELETE-LIVEDESK-LOCAL-DATA') {
4462
- res.status(400).json({ ok: false, error: 'privacy-delete-confirmation-required' });
4463
- return;
4464
- }
4465
- try {
4466
- const providerLogout = await revokeRuntimeProviderSession();
4467
- const hostTarget = runtimeRole === 'hub'
4468
- ? await clearHubHostTarget('privacy-delete')
4469
- : { ok: true, active: false };
4470
- clearRuntimeSession();
4471
- for (const device of remoteHub.getSecurityStatus().devices) {
4472
- remoteHub.disconnectDevice(device.deviceId, 'privacy-local-data-delete');
4473
- }
4474
- const trustedDeviceCredentials = remoteHub.clearDeviceCredentials();
4475
- const transferJobs = hubTransferJobs.clear();
4476
- const captures = await captureStore.removeAll();
4477
- const sharedFolders = await hubSharedFolders.clear();
4478
- await agentAuditStore.clear();
4479
- await securityAuditStore.reset();
4480
- securityAuditHealthy = true;
4481
- await securityAuditStore.record({
4482
- action: 'privacy.local-data.deleted',
4483
- phase: 'complete',
4484
- result: 'success',
4485
- authMethod: 'local-admin-session',
4486
- details: {
4487
- capturesRemoved: captures.removed,
4488
- captureBytesRemoved: captures.removedBytes,
4489
- sharedFolderDefinitionsRemoved: sharedFolders.removed,
4490
- trustedDeviceCredentialsRemoved: trustedDeviceCredentials,
4491
- transferJobRecordsRemoved: transferJobs.removed,
4492
- providerSessionRevoked: providerLogout.revoked === true
4493
- }
4494
- });
4495
- res.json({
4496
- ok: true,
4497
- localDeleted: true,
4498
- providerLogout,
4499
- hostTarget,
4500
- captures,
4501
- sharedFolders,
4502
- trustedDeviceCredentials,
4503
- transferJobs,
4504
- preserved: ['settings-preferences', 'hub-device-identity', 'application-binaries']
4505
- });
4506
- } catch (error) {
4507
- securityAuditHealthy = false;
4508
- res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-delete-failed').slice(0, 120) });
4509
- }
4510
- });
4511
-
4512
3908
  app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
4513
3909
  noStore(res);
4514
- const disconnected = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
4515
- const credentialRevoked = remoteHub.revokeDeviceCredential(req.params.deviceId, 'trusted-device-revoked');
4516
- const revoked = disconnected === true || credentialRevoked === true;
4517
- recordSecurityAudit({
4518
- action: 'security.device.revoked',
4519
- phase: 'complete',
4520
- result: revoked ? 'success' : 'not-found',
4521
- deviceId: req.params.deviceId,
4522
- authMethod: 'local-admin-session'
4523
- });
4524
- res.json({ ok: revoked, revoked, deviceId: req.params.deviceId });
3910
+ const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
3911
+ res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
4525
3912
  });
4526
3913
 
4527
3914
  app.post('/api/security/revoke-all', (_req, res) => {
@@ -4529,11 +3916,8 @@ app.post('/api/security/revoke-all', (_req, res) => {
4529
3916
  const devices = remoteHub.listDevices({ includeDataUrl: false });
4530
3917
  let revoked = 0;
4531
3918
  for (const device of devices) {
4532
- const disconnected = device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked');
4533
- const credentialRevoked = remoteHub.revokeDeviceCredential(device.deviceId, 'trusted-devices-revoked');
4534
- if (disconnected || credentialRevoked) revoked += 1;
3919
+ if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
4535
3920
  }
4536
- recordSecurityAudit({ action: 'security.devices.revoke-all', phase: 'complete', result: 'success', authMethod: 'local-admin-session', details: { revoked } });
4537
3921
  res.json({ ok: true, revoked });
4538
3922
  });
4539
3923
 
@@ -4897,16 +4281,8 @@ app.post('/api/runtime/restart', (_req, res) => {
4897
4281
  }, 150);
4898
4282
  });
4899
4283
 
4900
- app.post('/api/runtime/shutdown', (req, res) => {
4284
+ app.post('/api/runtime/shutdown', (_req, res) => {
4901
4285
  noStore(res);
4902
- recordSecurityAudit({
4903
- action: 'runtime.shutdown',
4904
- phase: 'requested',
4905
- result: 'accepted',
4906
- authMethod: req.liveDeskLauncherShutdown === true
4907
- ? 'launcher-internal-shutdown-token'
4908
- : 'local-admin-session'
4909
- });
4910
4286
  runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
4911
4287
  res.json({ ok: true, shuttingDown: true, role: runtimeRole });
4912
4288
  setTimeout(() => {
@@ -4945,8 +4321,6 @@ app.post('/api/auth/session', async (req, res) => {
4945
4321
  const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
4946
4322
  try {
4947
4323
  const user = await verifySupabaseUser(accessToken);
4948
- runtimeSessionGeneration += 1;
4949
- runtimeRefreshPromise = null;
4950
4324
  runtimeAccessToken = accessToken;
4951
4325
  runtimeRefreshToken = refreshToken;
4952
4326
  runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
@@ -4963,26 +4337,9 @@ app.post('/api/auth/session', async (req, res) => {
4963
4337
  if (runtimeRole === 'hub') {
4964
4338
  startHubHostTargetLeaseRenewal();
4965
4339
  }
4966
- recordSecurityAudit({
4967
- action: 'auth.session.established',
4968
- phase: 'complete',
4969
- result: 'success',
4970
- actorAccountId: user.id,
4971
- actorUserId: user.id,
4972
- authMethod: 'supabase-access-refresh',
4973
- requestHash: requestAuditHash({ userId: user.id, expiresAt })
4974
- });
4975
4340
  res.json({ ok: true, authenticated: true, persisted, userId: user.id, role: runtimeRole, hostTarget });
4976
4341
  } catch (error) {
4977
4342
  const message = error instanceof Error ? error.message : String(error);
4978
- recordSecurityAudit({
4979
- action: 'auth.session.establish',
4980
- phase: 'complete',
4981
- result: 'rejected',
4982
- reason: message,
4983
- authMethod: 'supabase-access-refresh',
4984
- requestHash: requestAuditHash({ hasAccessToken: Boolean(req.body?.accessToken || req.body?.access_token), hasRefreshToken: Boolean(req.body?.refreshToken || req.body?.refresh_token) })
4985
- });
4986
4343
  const status = authVerificationHttpStatus(message);
4987
4344
  res.status(status).json({ ok: false, error: message });
4988
4345
  }
@@ -5278,30 +4635,11 @@ function startHubHostTargetLeaseRenewal() {
5278
4635
 
5279
4636
  app.delete('/api/auth/session', async (_req, res) => {
5280
4637
  noStore(res);
5281
- const actorUserId = runtimeManager.getSnapshot().userId || '';
5282
4638
  const hostTarget = runtimeRole === 'hub'
5283
4639
  ? await clearHubHostTarget('logout')
5284
4640
  : { ok: true, active: false };
5285
- const providerLogout = await revokeRuntimeProviderSession();
5286
4641
  clearRuntimeSession();
5287
- recordSecurityAudit({
5288
- action: 'auth.session.revoked',
5289
- phase: 'complete',
5290
- result: providerLogout.ok ? 'success' : 'provider-error',
5291
- actorAccountId: actorUserId,
5292
- actorUserId,
5293
- reason: providerLogout.error || '',
5294
- authMethod: 'supabase-local-signout',
5295
- details: { localCleared: true, providerRevoked: providerLogout.revoked === true, alreadyInvalid: providerLogout.alreadyInvalid === true }
5296
- });
5297
- res.status(providerLogout.ok ? 200 : 502).json({
5298
- ok: providerLogout.ok,
5299
- authenticated: false,
5300
- localCleared: true,
5301
- role: runtimeRole,
5302
- hostTarget,
5303
- providerLogout
5304
- });
4642
+ res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
5305
4643
  });
5306
4644
 
5307
4645
  app.get('/api/hub/status', (_req, res) => {
@@ -5652,33 +4990,7 @@ app.post('/api/remote/files/from-hub', requireHubFeatureAccess, (req, res) => {
5652
4990
  const job = hubTransferJobs.create({
5653
4991
  itemIds,
5654
4992
  deviceIds,
5655
- remoteDirectory: req.body?.remoteDirectory,
5656
- onComplete: async ({ completed, job: completedJob }) => {
5657
- await recordSecurityAuditRequired({
5658
- action: 'remote.file.job',
5659
- phase: 'complete',
5660
- result: completed ? 'success' : 'failed',
5661
- reason: completed ? '' : completedJob?.error || 'file-transfer-failed',
5662
- deviceIds,
5663
- sessionId: completedJob?.jobId || '',
5664
- requestHash: requestAuditHash({
5665
- jobId: completedJob?.jobId || '',
5666
- deviceIds,
5667
- remoteDirectory: req.body?.remoteDirectory || ''
5668
- }),
5669
- authMethod: 'local-admin-session',
5670
- details: {
5671
- state: completedJob?.state || '',
5672
- totalFiles: Number(completedJob?.totalFiles || 0),
5673
- completedFiles: Number(completedJob?.completedFiles || 0),
5674
- totalBytes: Number(completedJob?.totalBytes || 0),
5675
- sentBytes: Number(completedJob?.sentBytes || 0),
5676
- failedTargets: Array.isArray(completedJob?.failedTargets)
5677
- ? completedJob.failedTargets.length
5678
- : 0
5679
- }
5680
- });
5681
- }
4993
+ remoteDirectory: req.body?.remoteDirectory
5682
4994
  });
5683
4995
  res.status(202).json({ ok: true, jobId: job.jobId, state: job.state });
5684
4996
  } catch (error) {
@@ -5760,80 +5072,56 @@ app.post('/api/remote/filesystem/shared-folders/:folderId/sync', requireHubFeatu
5760
5072
  }
5761
5073
  });
5762
5074
 
5763
- app.post('/api/remote/files/transfer', requireHubFeatureAccess, async (req, res) => {
5075
+ app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
5764
5076
  noStore(res);
5765
5077
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5766
5078
  if (deviceIds.length === 0) {
5767
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5768
5079
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5769
5080
  return;
5770
5081
  }
5771
5082
  const normalized = normalizeTransferFiles(req.body?.files);
5772
5083
  if (!normalized.ok) {
5773
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5774
5084
  res.status(400).json({ ok: false, error: normalized.error, totalBytes: normalized.totalBytes || 0 });
5775
5085
  return;
5776
5086
  }
5777
5087
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5778
5088
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5779
5089
  const queuedAt = new Date().toISOString();
5780
- try {
5781
- const results = await Promise.all(deviceIds.map(async deviceId => ({
5782
- deviceId,
5783
- ...await remoteHub.sendCommandAwaitResult(deviceId, {
5784
- command: 'file.transfer',
5785
- payload: {
5786
- transferId,
5787
- remoteDirectory,
5788
- files: normalized.files,
5789
- totalBytes: normalized.totalBytes,
5790
- requestedAt: queuedAt
5791
- }
5792
- }, { timeoutMs: fileTransferCommandAckTimeoutMs })
5793
- })));
5794
- const queued = results.filter(result => result.queued).length;
5795
- const acknowledged = results.filter(result => result.acknowledged).length;
5796
- const success = acknowledged === deviceIds.length;
5797
- const partial = acknowledged > 0 && !success;
5798
- const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
5799
- await recordSecurityAuditRequired({
5800
- action: 'remote.file.transfer',
5801
- phase: 'complete',
5802
- result: success ? 'success' : partial ? 'partial' : 'rejected',
5803
- reason: error || '',
5804
- deviceIds,
5805
- sessionId: transferId,
5806
- requestHash: requestAuditHash({ transferId, remoteDirectory, files: normalized.files.map(file => ({ name: file.name, relativePath: file.relativePath, byteLength: file.size })), totalBytes: normalized.totalBytes }),
5807
- authMethod: 'local-admin-session',
5808
- details: { queued, acknowledged, total: deviceIds.length, totalBytes: normalized.totalBytes, files: normalized.files.length }
5809
- });
5810
- res.json({
5811
- ok: success,
5812
- transferId,
5813
- queued,
5814
- acknowledged,
5815
- total: deviceIds.length,
5816
- totalBytes: normalized.totalBytes,
5817
- files: normalized.files.length,
5818
- results,
5819
- error
5820
- });
5821
- } catch (error) {
5822
- res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
5823
- }
5090
+ const results = deviceIds.map(deviceId => ({
5091
+ deviceId,
5092
+ ...remoteHub.sendCommand(deviceId, {
5093
+ command: 'file.transfer',
5094
+ payload: {
5095
+ transferId,
5096
+ remoteDirectory,
5097
+ files: normalized.files,
5098
+ totalBytes: normalized.totalBytes,
5099
+ requestedAt: queuedAt
5100
+ }
5101
+ })
5102
+ }));
5103
+ const queued = results.filter(result => result.ok).length;
5104
+ res.json({
5105
+ ok: queued > 0,
5106
+ transferId,
5107
+ queued,
5108
+ total: deviceIds.length,
5109
+ totalBytes: normalized.totalBytes,
5110
+ files: normalized.files.length,
5111
+ results,
5112
+ error: queued > 0 ? undefined : 'no-transfer-queued'
5113
+ });
5824
5114
  });
5825
5115
 
5826
- app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) => {
5116
+ app.post('/api/remote/files/chunk', requireHubFeatureAccess, (req, res) => {
5827
5117
  noStore(res);
5828
5118
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5829
5119
  if (deviceIds.length === 0) {
5830
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5831
5120
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5832
5121
  return;
5833
5122
  }
5834
5123
  const normalized = normalizeTransferChunk(req.body || {});
5835
5124
  if (!normalized.ok) {
5836
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5837
5125
  res.status(400).json({ ok: false, error: normalized.error, byteLength: normalized.byteLength || 0 });
5838
5126
  return;
5839
5127
  }
@@ -5841,50 +5129,30 @@ app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) =>
5841
5129
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5842
5130
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5843
5131
  const queuedAt = new Date().toISOString();
5844
- try {
5845
- const results = await Promise.all(deviceIds.map(async deviceId => ({
5846
- deviceId,
5847
- ...await remoteHub.sendCommandAwaitResult(deviceId, {
5848
- command: 'file.transfer.chunk',
5849
- payload: {
5850
- transferId,
5851
- remoteDirectory,
5852
- ...normalized.chunk,
5853
- requestedAt: queuedAt
5854
- }
5855
- }, { timeoutMs: fileTransferCommandAckTimeoutMs })
5856
- })));
5857
- const queued = results.filter(result => result.queued).length;
5858
- const acknowledged = results.filter(result => result.acknowledged).length;
5859
- const success = acknowledged === deviceIds.length;
5860
- const partial = acknowledged > 0 && !success;
5861
- const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
5862
- await recordSecurityAuditRequired({
5863
- action: 'remote.file.chunk',
5864
- phase: normalized.chunk.final ? 'complete' : 'progress',
5865
- result: success ? 'success' : partial ? 'partial' : 'rejected',
5866
- reason: error || '',
5867
- deviceIds,
5868
- sessionId: transferId,
5869
- requestHash: requestAuditHash({ transferId, remoteDirectory, offset: normalized.chunk.offset, byteLength: normalized.chunk.byteLength, final: normalized.chunk.final }),
5870
- authMethod: 'local-admin-session',
5871
- details: { queued, acknowledged, total: deviceIds.length, byteLength: normalized.chunk.byteLength, offset: normalized.chunk.offset, final: normalized.chunk.final }
5872
- });
5873
- res.json({
5874
- ok: success,
5875
- transferId,
5876
- queued,
5877
- acknowledged,
5878
- total: deviceIds.length,
5879
- byteLength: normalized.chunk.byteLength,
5880
- offset: normalized.chunk.offset,
5881
- final: normalized.chunk.final,
5882
- results,
5883
- error
5884
- });
5885
- } catch (error) {
5886
- res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
5887
- }
5132
+ const results = deviceIds.map(deviceId => ({
5133
+ deviceId,
5134
+ ...remoteHub.sendCommand(deviceId, {
5135
+ command: 'file.transfer.chunk',
5136
+ payload: {
5137
+ transferId,
5138
+ remoteDirectory,
5139
+ ...normalized.chunk,
5140
+ requestedAt: queuedAt
5141
+ }
5142
+ })
5143
+ }));
5144
+ const queued = results.filter(result => result.ok).length;
5145
+ res.json({
5146
+ ok: queued > 0,
5147
+ transferId,
5148
+ queued,
5149
+ total: deviceIds.length,
5150
+ byteLength: normalized.chunk.byteLength,
5151
+ offset: normalized.chunk.offset,
5152
+ final: normalized.chunk.final,
5153
+ results,
5154
+ error: queued > 0 ? undefined : 'no-transfer-queued'
5155
+ });
5888
5156
  });
5889
5157
 
5890
5158
  const MANUAL_POWER_ACTIONS = new Set(['lock', 'sleep', 'restart', 'shutdown']);
@@ -6173,65 +5441,14 @@ if (existsSync(webIndexPath)) {
6173
5441
  });
6174
5442
  }
6175
5443
 
6176
- function isAuthorizedAdminWebSocket(req) {
6177
- if (!enforceLocalAdminAuth) return true;
6178
- const protocols = String(req.headers['sec-websocket-protocol'] || '')
6179
- .split(',')
6180
- .map(value => value.trim())
6181
- .filter(Boolean);
6182
- const adminToken = protocols.find(value => value.startsWith('livedesk-admin-'))?.slice('livedesk-admin-'.length) || '';
6183
- const csrfToken = protocols.find(value => value.startsWith('livedesk-admin-csrf-'))?.slice('livedesk-admin-csrf-'.length) || '';
6184
- return secureExactTestTokenMatches(adminToken, localAdminSessionToken)
6185
- && secureExactTestTokenMatches(csrfToken, localAdminCsrfToken);
6186
- }
6187
-
6188
5444
  httpServer.on('upgrade', (req, socket, head) => {
6189
5445
  if (!isTrustedBrowserRequest(req)) {
6190
5446
  socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
6191
5447
  socket.destroy();
6192
5448
  return;
6193
5449
  }
6194
- if (!isAuthorizedAdminWebSocket(req)) {
6195
- recordSecurityAudit({
6196
- action: 'local-admin.websocket',
6197
- phase: 'complete',
6198
- result: 'rejected',
6199
- reason: 'admin-session-required',
6200
- requestHash: requestAuditHash({ path: req.url || '' }),
6201
- authMethod: 'local-admin-websocket'
6202
- });
6203
- socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
6204
- socket.destroy();
6205
- return;
6206
- }
6207
- let parsed;
6208
5450
  try {
6209
- parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
6210
- } catch {
6211
- socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
6212
- socket.destroy();
6213
- return;
6214
- }
6215
- const websocketPath = parsed.pathname;
6216
- if (![
6217
- '/api/remote/frames/ws',
6218
- '/api/remote/atlas/ws',
6219
- '/api/remote/input/ws',
6220
- '/api/remote/audio/ws'
6221
- ].includes(websocketPath)) {
6222
- socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
6223
- socket.destroy();
6224
- return;
6225
- }
6226
- void recordSecurityAuditRequired({
6227
- action: 'local-admin.websocket',
6228
- phase: 'accepted',
6229
- result: 'accepted',
6230
- sessionId: localAdminSessionId,
6231
- requestHash: requestAuditHash({ path: websocketPath }),
6232
- authMethod: 'local-admin-websocket',
6233
- details: { path: websocketPath }
6234
- }).then(() => {
5451
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
6235
5452
  if (parsed.pathname === '/api/remote/frames/ws') {
6236
5453
  frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
6237
5454
  return;
@@ -6248,10 +5465,12 @@ httpServer.on('upgrade', (req, socket, head) => {
6248
5465
  audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
6249
5466
  return;
6250
5467
  }
6251
- }).catch(() => {
6252
- socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
5468
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
6253
5469
  socket.destroy();
6254
- });
5470
+ } catch {
5471
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
5472
+ socket.destroy();
5473
+ }
6255
5474
  });
6256
5475
 
6257
5476
  frameWss.on('connection', (ws, req) => {
@@ -6277,8 +5496,6 @@ frameWss.on('connection', (ws, req) => {
6277
5496
  const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
6278
5497
  if (payload?.type === 'subscribe') {
6279
5498
  updateFrameSubscription(ws, payload);
6280
- } else if (payload?.type === 'refresh-live') {
6281
- refreshFrameSubscriptionLive(ws, payload);
6282
5499
  } else if (payload?.type === 'restart-live') {
6283
5500
  restartFrameSubscriptionLive(ws, payload);
6284
5501
  }
@@ -6368,17 +5585,12 @@ inputWss.on('connection', ws => {
6368
5585
  inputClients.add(ws);
6369
5586
  ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
6370
5587
  ws.liveDeskInputDeviceIds = new Set();
6371
- ws.liveDeskInputAuditReady = false;
6372
5588
  try {
6373
5589
  ws._socket?.setNoDelay?.(true);
6374
5590
  } catch {
6375
5591
  // Best-effort latency hint for browser input sockets.
6376
5592
  }
6377
5593
  ws.on('message', data => {
6378
- if (ws.liveDeskInputAuditReady !== true) {
6379
- sendJson(ws, { type: 'RemoteInputError', error: 'security-audit-pending' });
6380
- return;
6381
- }
6382
5594
  let payload;
6383
5595
  try {
6384
5596
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
@@ -6419,43 +5631,19 @@ inputWss.on('connection', ws => {
6419
5631
  });
6420
5632
  });
6421
5633
  const releaseBrowserInputOwner = reason => {
6422
- if (ws.liveDeskInputReleased) return;
6423
- ws.liveDeskInputReleased = true;
6424
5634
  inputClients.delete(ws);
6425
5635
  for (const deviceId of ws.liveDeskInputDeviceIds || []) {
6426
5636
  remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
6427
5637
  }
6428
- void recordSecurityAuditRequired({
6429
- action: 'remote.control.browser-session',
6430
- phase: 'complete',
6431
- result: 'success',
6432
- reason,
6433
- deviceIds: [...(ws.liveDeskInputDeviceIds || [])],
6434
- sessionId: ws.liveDeskInputClientId,
6435
- authMethod: 'local-admin-websocket'
6436
- }).catch(() => {
6437
- // The global fail-closed gate is marked by recordSecurityAuditRequired.
6438
- });
6439
5638
  ws.liveDeskInputDeviceIds?.clear?.();
6440
5639
  };
6441
5640
  ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
6442
5641
  ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
6443
- void recordSecurityAuditRequired({
6444
- action: 'remote.control.browser-session',
6445
- phase: 'start',
6446
- result: 'success',
6447
- sessionId: ws.liveDeskInputClientId,
6448
- authMethod: 'local-admin-websocket'
6449
- }).then(() => {
6450
- ws.liveDeskInputAuditReady = true;
6451
- sendJson(ws, {
6452
- type: 'RemoteInputSocketReady',
6453
- protocol: 'livedesk.remote.input.json.v1',
6454
- clientId: ws.liveDeskInputClientId,
6455
- timestamp: new Date().toISOString()
6456
- });
6457
- }).catch(() => {
6458
- ws.close(1011, 'security-audit-unavailable');
5642
+ sendJson(ws, {
5643
+ type: 'RemoteInputSocketReady',
5644
+ protocol: 'livedesk.remote.input.json.v1',
5645
+ clientId: ws.liveDeskInputClientId,
5646
+ timestamp: new Date().toISOString()
6459
5647
  });
6460
5648
  });
6461
5649
 
@@ -6552,7 +5740,6 @@ function shutdownHub(signal) {
6552
5740
  const startedAt = Date.now();
6553
5741
  console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
6554
5742
  if (roleWatchTimer) clearInterval(roleWatchTimer);
6555
- clearInterval(captureRetentionTimer);
6556
5743
  clearInterval(browserWebSocketHeartbeatTimer);
6557
5744
  runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
6558
5745
  atlasClients.clear();