@livedesk/hub 0.1.36 → 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,137 +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;
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 persisted = readPersistedRuntimeSession();
655
- const normalized = normalizeRuntimeAuthSession(persisted, { requireRefreshToken: true });
656
- const userId = String(normalized.ok ? normalized.session?.user?.id || '' : '').trim();
657
- if (!normalized.ok || !userId) {
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
- if (!accessToken) return { ok: false, error: 'provider-session-token-unavailable' };
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
- const response = await fetchAuthResponse(
703
- fetch,
704
- `${supabaseUrl}/auth/v1/logout?scope=local`,
705
- {
706
- method: 'POST',
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
- return { ok: false, error: String(error?.message || error) };
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
- .then(() => captureStore.enforceRetention())
902
- .catch(error => {
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: fileTransferCommandAckTimeoutMs,
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, X-LiveDesk-Admin-Session, X-LiveDesk-Admin-CSRF, X-LiveDesk-CSRF');
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
- const parseHubJsonBody = express.json({ limit: '32mb' });
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);
@@ -3217,37 +2746,6 @@ function updateFrameSubscription(ws, payload = {}) {
3217
2746
  }
3218
2747
  }
3219
2748
 
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
- monitorSelections: {
3239
- ...(ws.liveDeskLiveOptions?.monitorSelections || {}),
3240
- ...(payload.monitorSelections || {})
3241
- }
3242
- });
3243
- for (const deviceId of targetIds.slice(0, 80)) {
3244
- startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
3245
- ...ws.liveDeskLiveOptions,
3246
- reuseExisting: true
3247
- });
3248
- }
3249
- }
3250
-
3251
2749
  function startFrameSubscriptionLive(
3252
2750
  ws,
3253
2751
  reason = 'subscribe',
@@ -3348,7 +2846,6 @@ function startFrameSubscriptionLive(
3348
2846
  continue;
3349
2847
  }
3350
2848
  cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
3351
- const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
3352
2849
  started.push({
3353
2850
  deviceId: expectedBinding.deviceId,
3354
2851
  sessionId: expectedBinding.sessionId,
@@ -3357,8 +2854,7 @@ function startFrameSubscriptionLive(
3357
2854
  commandId: expectedBinding.commandId,
3358
2855
  captureGeneration: expectedBinding.captureGeneration,
3359
2856
  monitorIndex: expectedBinding.monitorIndex,
3360
- ready: expectedBinding.readySent === true,
3361
- replayedKeyFrame,
2857
+ ready: reusedFrameReady,
3362
2858
  fps: result.fps,
3363
2859
  reused: result.reused === true
3364
2860
  });
@@ -4186,22 +3682,6 @@ app.get('/api/health', (_req, res) => {
4186
3682
  res.json(buildHubHealthPayload());
4187
3683
  });
4188
3684
 
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
3685
  if (persistentSessionGcEnabled) {
4206
3686
  app.post('/api/test/persistent-session/gc', (req, res) => {
4207
3687
  noStore(res);
@@ -4255,15 +3735,6 @@ app.patch('/api/settings', async (req, res) => {
4255
3735
  await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
4256
3736
  }
4257
3737
  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
3738
  res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
4268
3739
  } catch (error) {
4269
3740
  if (error instanceof SettingsConflictError) {
@@ -4434,124 +3905,10 @@ app.get('/api/security/trusted-devices', (_req, res) => {
4434
3905
  res.json({ ok: true, devices });
4435
3906
  });
4436
3907
 
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
3908
  app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
4543
3909
  noStore(res);
4544
- const disconnected = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
4545
- const credentialRevoked = remoteHub.revokeDeviceCredential(req.params.deviceId, 'trusted-device-revoked');
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 });
3910
+ const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
3911
+ res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
4555
3912
  });
4556
3913
 
4557
3914
  app.post('/api/security/revoke-all', (_req, res) => {
@@ -4559,11 +3916,8 @@ app.post('/api/security/revoke-all', (_req, res) => {
4559
3916
  const devices = remoteHub.listDevices({ includeDataUrl: false });
4560
3917
  let revoked = 0;
4561
3918
  for (const device of devices) {
4562
- const disconnected = device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked');
4563
- const credentialRevoked = remoteHub.revokeDeviceCredential(device.deviceId, 'trusted-devices-revoked');
4564
- if (disconnected || credentialRevoked) revoked += 1;
3919
+ if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
4565
3920
  }
4566
- recordSecurityAudit({ action: 'security.devices.revoke-all', phase: 'complete', result: 'success', authMethod: 'local-admin-session', details: { revoked } });
4567
3921
  res.json({ ok: true, revoked });
4568
3922
  });
4569
3923
 
@@ -4927,16 +4281,8 @@ app.post('/api/runtime/restart', (_req, res) => {
4927
4281
  }, 150);
4928
4282
  });
4929
4283
 
4930
- app.post('/api/runtime/shutdown', (req, res) => {
4284
+ app.post('/api/runtime/shutdown', (_req, res) => {
4931
4285
  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
4286
  runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
4941
4287
  res.json({ ok: true, shuttingDown: true, role: runtimeRole });
4942
4288
  setTimeout(() => {
@@ -4975,8 +4321,6 @@ app.post('/api/auth/session', async (req, res) => {
4975
4321
  const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
4976
4322
  try {
4977
4323
  const user = await verifySupabaseUser(accessToken);
4978
- runtimeSessionGeneration += 1;
4979
- runtimeRefreshPromise = null;
4980
4324
  runtimeAccessToken = accessToken;
4981
4325
  runtimeRefreshToken = refreshToken;
4982
4326
  runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
@@ -4993,26 +4337,9 @@ app.post('/api/auth/session', async (req, res) => {
4993
4337
  if (runtimeRole === 'hub') {
4994
4338
  startHubHostTargetLeaseRenewal();
4995
4339
  }
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
4340
  res.json({ ok: true, authenticated: true, persisted, userId: user.id, role: runtimeRole, hostTarget });
5006
4341
  } catch (error) {
5007
4342
  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
4343
  const status = authVerificationHttpStatus(message);
5017
4344
  res.status(status).json({ ok: false, error: message });
5018
4345
  }
@@ -5308,30 +4635,11 @@ function startHubHostTargetLeaseRenewal() {
5308
4635
 
5309
4636
  app.delete('/api/auth/session', async (_req, res) => {
5310
4637
  noStore(res);
5311
- const actorUserId = runtimeManager.getSnapshot().userId || '';
5312
4638
  const hostTarget = runtimeRole === 'hub'
5313
4639
  ? await clearHubHostTarget('logout')
5314
4640
  : { ok: true, active: false };
5315
- const providerLogout = await revokeRuntimeProviderSession();
5316
4641
  clearRuntimeSession();
5317
- recordSecurityAudit({
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
- });
4642
+ res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
5335
4643
  });
5336
4644
 
5337
4645
  app.get('/api/hub/status', (_req, res) => {
@@ -5682,33 +4990,7 @@ app.post('/api/remote/files/from-hub', requireHubFeatureAccess, (req, res) => {
5682
4990
  const job = hubTransferJobs.create({
5683
4991
  itemIds,
5684
4992
  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
- }
4993
+ remoteDirectory: req.body?.remoteDirectory
5712
4994
  });
5713
4995
  res.status(202).json({ ok: true, jobId: job.jobId, state: job.state });
5714
4996
  } catch (error) {
@@ -5790,80 +5072,56 @@ app.post('/api/remote/filesystem/shared-folders/:folderId/sync', requireHubFeatu
5790
5072
  }
5791
5073
  });
5792
5074
 
5793
- app.post('/api/remote/files/transfer', requireHubFeatureAccess, async (req, res) => {
5075
+ app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
5794
5076
  noStore(res);
5795
5077
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5796
5078
  if (deviceIds.length === 0) {
5797
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5798
5079
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5799
5080
  return;
5800
5081
  }
5801
5082
  const normalized = normalizeTransferFiles(req.body?.files);
5802
5083
  if (!normalized.ok) {
5803
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5804
5084
  res.status(400).json({ ok: false, error: normalized.error, totalBytes: normalized.totalBytes || 0 });
5805
5085
  return;
5806
5086
  }
5807
5087
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5808
5088
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5809
5089
  const queuedAt = new Date().toISOString();
5810
- try {
5811
- const results = await Promise.all(deviceIds.map(async deviceId => ({
5812
- deviceId,
5813
- ...await remoteHub.sendCommandAwaitResult(deviceId, {
5814
- command: 'file.transfer',
5815
- payload: {
5816
- transferId,
5817
- remoteDirectory,
5818
- files: normalized.files,
5819
- totalBytes: normalized.totalBytes,
5820
- requestedAt: queuedAt
5821
- }
5822
- }, { timeoutMs: fileTransferCommandAckTimeoutMs })
5823
- })));
5824
- const queued = results.filter(result => result.queued).length;
5825
- const acknowledged = results.filter(result => result.acknowledged).length;
5826
- const success = acknowledged === deviceIds.length;
5827
- const partial = acknowledged > 0 && !success;
5828
- const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
5829
- await recordSecurityAuditRequired({
5830
- action: 'remote.file.transfer',
5831
- phase: 'complete',
5832
- result: success ? 'success' : partial ? 'partial' : 'rejected',
5833
- reason: error || '',
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
- }
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
+ });
5854
5114
  });
5855
5115
 
5856
- app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) => {
5116
+ app.post('/api/remote/files/chunk', requireHubFeatureAccess, (req, res) => {
5857
5117
  noStore(res);
5858
5118
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5859
5119
  if (deviceIds.length === 0) {
5860
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5861
5120
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5862
5121
  return;
5863
5122
  }
5864
5123
  const normalized = normalizeTransferChunk(req.body || {});
5865
5124
  if (!normalized.ok) {
5866
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5867
5125
  res.status(400).json({ ok: false, error: normalized.error, byteLength: normalized.byteLength || 0 });
5868
5126
  return;
5869
5127
  }
@@ -5871,50 +5129,30 @@ app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) =>
5871
5129
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5872
5130
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5873
5131
  const queuedAt = new Date().toISOString();
5874
- try {
5875
- const results = await Promise.all(deviceIds.map(async deviceId => ({
5876
- deviceId,
5877
- ...await remoteHub.sendCommandAwaitResult(deviceId, {
5878
- command: 'file.transfer.chunk',
5879
- payload: {
5880
- transferId,
5881
- remoteDirectory,
5882
- ...normalized.chunk,
5883
- requestedAt: queuedAt
5884
- }
5885
- }, { timeoutMs: fileTransferCommandAckTimeoutMs })
5886
- })));
5887
- const queued = results.filter(result => result.queued).length;
5888
- const acknowledged = results.filter(result => result.acknowledged).length;
5889
- const success = acknowledged === deviceIds.length;
5890
- const partial = acknowledged > 0 && !success;
5891
- const error = success ? undefined : partial ? 'partial-transfer-failed' : 'no-transfer-acknowledged';
5892
- await recordSecurityAuditRequired({
5893
- action: 'remote.file.chunk',
5894
- phase: normalized.chunk.final ? 'complete' : 'progress',
5895
- result: success ? 'success' : partial ? 'partial' : 'rejected',
5896
- reason: error || '',
5897
- deviceIds,
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
- }
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
+ });
5918
5156
  });
5919
5157
 
5920
5158
  const MANUAL_POWER_ACTIONS = new Set(['lock', 'sleep', 'restart', 'shutdown']);
@@ -6203,65 +5441,14 @@ if (existsSync(webIndexPath)) {
6203
5441
  });
6204
5442
  }
6205
5443
 
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
5444
  httpServer.on('upgrade', (req, socket, head) => {
6219
5445
  if (!isTrustedBrowserRequest(req)) {
6220
5446
  socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
6221
5447
  socket.destroy();
6222
5448
  return;
6223
5449
  }
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
5450
  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(() => {
5451
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
6265
5452
  if (parsed.pathname === '/api/remote/frames/ws') {
6266
5453
  frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
6267
5454
  return;
@@ -6278,10 +5465,12 @@ httpServer.on('upgrade', (req, socket, head) => {
6278
5465
  audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
6279
5466
  return;
6280
5467
  }
6281
- }).catch(() => {
6282
- 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');
6283
5469
  socket.destroy();
6284
- });
5470
+ } catch {
5471
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
5472
+ socket.destroy();
5473
+ }
6285
5474
  });
6286
5475
 
6287
5476
  frameWss.on('connection', (ws, req) => {
@@ -6307,8 +5496,6 @@ frameWss.on('connection', (ws, req) => {
6307
5496
  const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
6308
5497
  if (payload?.type === 'subscribe') {
6309
5498
  updateFrameSubscription(ws, payload);
6310
- } else if (payload?.type === 'refresh-live') {
6311
- refreshFrameSubscriptionLive(ws, payload);
6312
5499
  } else if (payload?.type === 'restart-live') {
6313
5500
  restartFrameSubscriptionLive(ws, payload);
6314
5501
  }
@@ -6398,17 +5585,12 @@ inputWss.on('connection', ws => {
6398
5585
  inputClients.add(ws);
6399
5586
  ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
6400
5587
  ws.liveDeskInputDeviceIds = new Set();
6401
- ws.liveDeskInputAuditReady = false;
6402
5588
  try {
6403
5589
  ws._socket?.setNoDelay?.(true);
6404
5590
  } catch {
6405
5591
  // Best-effort latency hint for browser input sockets.
6406
5592
  }
6407
5593
  ws.on('message', data => {
6408
- if (ws.liveDeskInputAuditReady !== true) {
6409
- sendJson(ws, { type: 'RemoteInputError', error: 'security-audit-pending' });
6410
- return;
6411
- }
6412
5594
  let payload;
6413
5595
  try {
6414
5596
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
@@ -6449,43 +5631,19 @@ inputWss.on('connection', ws => {
6449
5631
  });
6450
5632
  });
6451
5633
  const releaseBrowserInputOwner = reason => {
6452
- if (ws.liveDeskInputReleased) return;
6453
- ws.liveDeskInputReleased = true;
6454
5634
  inputClients.delete(ws);
6455
5635
  for (const deviceId of ws.liveDeskInputDeviceIds || []) {
6456
5636
  remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
6457
5637
  }
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
5638
  ws.liveDeskInputDeviceIds?.clear?.();
6470
5639
  };
6471
5640
  ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
6472
5641
  ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
6473
- void recordSecurityAuditRequired({
6474
- action: 'remote.control.browser-session',
6475
- phase: 'start',
6476
- result: 'success',
6477
- sessionId: ws.liveDeskInputClientId,
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');
5642
+ sendJson(ws, {
5643
+ type: 'RemoteInputSocketReady',
5644
+ protocol: 'livedesk.remote.input.json.v1',
5645
+ clientId: ws.liveDeskInputClientId,
5646
+ timestamp: new Date().toISOString()
6489
5647
  });
6490
5648
  });
6491
5649
 
@@ -6501,9 +5659,6 @@ httpServer.listen(httpPort, httpHost, () => {
6501
5659
  console.log(`[LiveDesk Hub] Version ${managerVersion}`);
6502
5660
  console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
6503
5661
  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
5662
  });
6508
5663
 
6509
5664
  const roleWatchTimer = runtimeRole === 'hub'
@@ -6585,7 +5740,6 @@ function shutdownHub(signal) {
6585
5740
  const startedAt = Date.now();
6586
5741
  console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
6587
5742
  if (roleWatchTimer) clearInterval(roleWatchTimer);
6588
- clearInterval(captureRetentionTimer);
6589
5743
  clearInterval(browserWebSocketHeartbeatTimer);
6590
5744
  runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
6591
5745
  atlasClients.clear();