@livedesk/hub 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
@@ -3181,7 +2710,7 @@ function enqueueFramePacketForClient(ws, packetOwner, meta) {
3181
2710
  }
3182
2711
  // FRAME_LANE_GENERATION_CONTRACT_END
3183
2712
 
3184
- function updateFrameSubscription(ws, payload = {}) {
2713
+ function updateFrameSubscription(ws, payload = {}) {
3185
2714
  const previousDeviceIds = ws.liveDeskDeviceIds instanceof Set
3186
2715
  ? new Set(ws.liveDeskDeviceIds)
3187
2716
  : new Set();
@@ -3214,41 +2743,43 @@ function updateFrameSubscription(ws, payload = {}) {
3214
2743
  const targetIds = newlySubscribedIds.length > 0 ? newlySubscribedIds : deviceIds;
3215
2744
  for (const deviceId of targetIds.slice(0, 80)) {
3216
2745
  startFrameSubscriptionLive(ws, 'subscribe', deviceId, { reuseExisting: true });
3217
- }
3218
- }
3219
-
3220
- function refreshFrameSubscriptionLive(ws, payload = {}) {
3221
- if (!ws || ws.readyState !== 1) {
3222
- return;
3223
- }
3224
- const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
3225
- const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
3226
- const targetIds = requestedIds.length > 0
3227
- ? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
3228
- : subscribedIds;
3229
- if (targetIds.length === 0) {
3230
- return;
3231
- }
3232
- ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
3233
- payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
3234
- ));
3235
- ws.liveDeskLiveOptions = normalizeLiveOptions({
3236
- ...(ws.liveDeskLiveOptions || {}),
3237
- ...payload,
3238
- 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
- function startFrameSubscriptionLive(
2746
+ }
2747
+ }
2748
+
2749
+ function refreshFrameSubscriptionLive(ws, payload = {}) {
2750
+ if (!ws || ws.readyState !== 1) {
2751
+ return;
2752
+ }
2753
+ const requestedIds = normalizeDeviceIds(payload.deviceIds ?? payload.devices ?? payload.deviceId);
2754
+ const subscribedIds = [...(ws.liveDeskDeviceIds || [])];
2755
+ const targetIds = requestedIds.length > 0
2756
+ ? requestedIds.filter(deviceId => ws.liveDeskDeviceIds?.has?.(deviceId))
2757
+ : subscribedIds;
2758
+ if (targetIds.length === 0) {
2759
+ return;
2760
+ }
2761
+ ws.liveDeskAutoStart = /^(1|true|yes|on|live)$/i.test(String(
2762
+ payload.autoStartLive ?? payload.startLive ?? ws.liveDeskAutoStart ?? ''
2763
+ ));
2764
+ ws.liveDeskLiveOptions = normalizeLiveOptions({
2765
+ ...(ws.liveDeskLiveOptions || {}),
2766
+ ...payload,
2767
+ forceRestart: false,
2768
+ reuseExisting: true,
2769
+ monitorSelections: {
2770
+ ...(ws.liveDeskLiveOptions?.monitorSelections || {}),
2771
+ ...(payload.monitorSelections || {})
2772
+ }
2773
+ });
2774
+ for (const deviceId of targetIds.slice(0, 80)) {
2775
+ startFrameSubscriptionLive(ws, 'watchdog', deviceId, {
2776
+ ...ws.liveDeskLiveOptions,
2777
+ reuseExisting: true
2778
+ });
2779
+ }
2780
+ }
2781
+
2782
+ function startFrameSubscriptionLive(
3252
2783
  ws,
3253
2784
  reason = 'subscribe',
3254
2785
  onlyDeviceId = '',
@@ -3348,7 +2879,6 @@ function startFrameSubscriptionLive(
3348
2879
  continue;
3349
2880
  }
3350
2881
  cancelPendingFrameStreamStop(deviceId, result.streamId, String(liveOptions.streamPurpose || 'wall'));
3351
- const replayedKeyFrame = replayExpectedBindingKeyFrame(ws, expectedBinding);
3352
2882
  started.push({
3353
2883
  deviceId: expectedBinding.deviceId,
3354
2884
  sessionId: expectedBinding.sessionId,
@@ -3357,8 +2887,7 @@ function startFrameSubscriptionLive(
3357
2887
  commandId: expectedBinding.commandId,
3358
2888
  captureGeneration: expectedBinding.captureGeneration,
3359
2889
  monitorIndex: expectedBinding.monitorIndex,
3360
- ready: expectedBinding.readySent === true,
3361
- replayedKeyFrame,
2890
+ ready: reusedFrameReady,
3362
2891
  fps: result.fps,
3363
2892
  reused: result.reused === true
3364
2893
  });
@@ -4186,22 +3715,6 @@ app.get('/api/health', (_req, res) => {
4186
3715
  res.json(buildHubHealthPayload());
4187
3716
  });
4188
3717
 
4189
- app.get('/api/security/session', (req, res) => {
4190
- noStore(res);
4191
- if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostname(requestHostname(req))) {
4192
- res.status(403).json({ ok: false, error: 'local-admin-session-loopback-only' });
4193
- return;
4194
- }
4195
- res.json({
4196
- ok: true,
4197
- sessionId: localAdminSessionId,
4198
- adminToken: localAdminSessionToken,
4199
- csrfToken: localAdminCsrfToken,
4200
- issuedAt: new Date().toISOString(),
4201
- expiresOnProcessExit: true
4202
- });
4203
- });
4204
-
4205
3718
  if (persistentSessionGcEnabled) {
4206
3719
  app.post('/api/test/persistent-session/gc', (req, res) => {
4207
3720
  noStore(res);
@@ -4255,15 +3768,6 @@ app.patch('/api/settings', async (req, res) => {
4255
3768
  await agentSettingsStore.update({ enabled: record.settings.agent?.enabled === true });
4256
3769
  }
4257
3770
  remoteHub.refreshDevicePolicies();
4258
- void captureStore.enforceRetention().catch(() => undefined);
4259
- recordSecurityAudit({
4260
- action: 'settings.security-policy.updated',
4261
- phase: 'complete',
4262
- result: 'success',
4263
- requestHash: requestAuditHash(patch),
4264
- authMethod: 'local-admin-session',
4265
- details: { revision: record.revision, sections: Object.keys(patch) }
4266
- });
4267
3771
  res.json({ ok: true, revision: record.revision, updatedAt: record.updatedAt, settings: record.settings });
4268
3772
  } catch (error) {
4269
3773
  if (error instanceof SettingsConflictError) {
@@ -4434,124 +3938,10 @@ app.get('/api/security/trusted-devices', (_req, res) => {
4434
3938
  res.json({ ok: true, devices });
4435
3939
  });
4436
3940
 
4437
- app.get('/api/security/rendezvous-issuer', (_req, res) => {
4438
- noStore(res);
4439
- const security = remoteHub.getSecurityStatus();
4440
- res.json({
4441
- ok: true,
4442
- hubId: security.hubId,
4443
- issuerKeyId: security.hubIssuerKeyId,
4444
- publicKey: security.hubPublicKey,
4445
- algorithm: 'P-256/SHA-256',
4446
- environmentVariable: 'LIVEDESK_UDP_RENDEZVOUS_TRUSTED_ISSUER_PUBLIC_KEYS'
4447
- });
4448
- });
4449
-
4450
- app.get('/api/security/audit', async (req, res) => {
4451
- noStore(res);
4452
- try {
4453
- res.json({
4454
- ok: true,
4455
- healthy: securityAuditHealthy,
4456
- verification: await securityAuditStore.verify(),
4457
- audit: await securityAuditStore.list({ limit: req.query?.limit, action: req.query?.action })
4458
- });
4459
- } catch (error) {
4460
- securityAuditHealthy = false;
4461
- res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
4462
- }
4463
- });
4464
-
4465
- app.get('/api/privacy/inventory', async (_req, res) => {
4466
- noStore(res);
4467
- try {
4468
- const [captures, sharedFolders, verification] = await Promise.all([
4469
- captureStore.list(),
4470
- hubSharedFolders.list(),
4471
- securityAuditStore.verify()
4472
- ]);
4473
- res.json({
4474
- ok: true,
4475
- inventory: {
4476
- authenticated: runtimeManager.getSnapshot().authenticated === true,
4477
- captures: captures.length,
4478
- sharedFolderDefinitions: sharedFolders.length,
4479
- trustedDeviceCredentials: remoteHub.getSecurityStatus().devices.length,
4480
- securityAuditRecords: verification.records,
4481
- agentAuditRetained: (await agentAuditStore.list({ limit: 500 })).length
4482
- }
4483
- });
4484
- } catch (error) {
4485
- res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-inventory-unavailable').slice(0, 120) });
4486
- }
4487
- });
4488
-
4489
- app.delete('/api/privacy/local-data', async (req, res) => {
4490
- noStore(res);
4491
- if (String(req.body?.confirmation || '') !== 'DELETE-LIVEDESK-LOCAL-DATA') {
4492
- res.status(400).json({ ok: false, error: 'privacy-delete-confirmation-required' });
4493
- return;
4494
- }
4495
- try {
4496
- const providerLogout = await revokeRuntimeProviderSession();
4497
- const hostTarget = runtimeRole === 'hub'
4498
- ? await clearHubHostTarget('privacy-delete')
4499
- : { ok: true, active: false };
4500
- clearRuntimeSession();
4501
- for (const device of remoteHub.getSecurityStatus().devices) {
4502
- remoteHub.disconnectDevice(device.deviceId, 'privacy-local-data-delete');
4503
- }
4504
- const trustedDeviceCredentials = remoteHub.clearDeviceCredentials();
4505
- const transferJobs = hubTransferJobs.clear();
4506
- const captures = await captureStore.removeAll();
4507
- const sharedFolders = await hubSharedFolders.clear();
4508
- await agentAuditStore.clear();
4509
- await securityAuditStore.reset();
4510
- securityAuditHealthy = true;
4511
- await securityAuditStore.record({
4512
- action: 'privacy.local-data.deleted',
4513
- phase: 'complete',
4514
- result: 'success',
4515
- authMethod: 'local-admin-session',
4516
- details: {
4517
- capturesRemoved: captures.removed,
4518
- captureBytesRemoved: captures.removedBytes,
4519
- sharedFolderDefinitionsRemoved: sharedFolders.removed,
4520
- trustedDeviceCredentialsRemoved: trustedDeviceCredentials,
4521
- transferJobRecordsRemoved: transferJobs.removed,
4522
- providerSessionRevoked: providerLogout.revoked === true
4523
- }
4524
- });
4525
- res.json({
4526
- ok: true,
4527
- localDeleted: true,
4528
- providerLogout,
4529
- hostTarget,
4530
- captures,
4531
- sharedFolders,
4532
- trustedDeviceCredentials,
4533
- transferJobs,
4534
- preserved: ['settings-preferences', 'hub-device-identity', 'application-binaries']
4535
- });
4536
- } catch (error) {
4537
- securityAuditHealthy = false;
4538
- res.status(503).json({ ok: false, error: String(error?.code || error?.message || 'privacy-delete-failed').slice(0, 120) });
4539
- }
4540
- });
4541
-
4542
3941
  app.delete('/api/security/trusted-devices/:deviceId', (req, res) => {
4543
3942
  noStore(res);
4544
- const 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 });
3943
+ const result = remoteHub.disconnectDevice(req.params.deviceId, 'trusted-device-revoked');
3944
+ res.json({ ok: result === true, revoked: result === true, deviceId: req.params.deviceId });
4555
3945
  });
4556
3946
 
4557
3947
  app.post('/api/security/revoke-all', (_req, res) => {
@@ -4559,11 +3949,8 @@ app.post('/api/security/revoke-all', (_req, res) => {
4559
3949
  const devices = remoteHub.listDevices({ includeDataUrl: false });
4560
3950
  let revoked = 0;
4561
3951
  for (const device of devices) {
4562
- 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;
3952
+ if (device.connected && remoteHub.disconnectDevice(device.deviceId, 'trusted-devices-revoked')) revoked += 1;
4565
3953
  }
4566
- recordSecurityAudit({ action: 'security.devices.revoke-all', phase: 'complete', result: 'success', authMethod: 'local-admin-session', details: { revoked } });
4567
3954
  res.json({ ok: true, revoked });
4568
3955
  });
4569
3956
 
@@ -4927,16 +4314,8 @@ app.post('/api/runtime/restart', (_req, res) => {
4927
4314
  }, 150);
4928
4315
  });
4929
4316
 
4930
- app.post('/api/runtime/shutdown', (req, res) => {
4317
+ app.post('/api/runtime/shutdown', (_req, res) => {
4931
4318
  noStore(res);
4932
- recordSecurityAudit({
4933
- action: 'runtime.shutdown',
4934
- phase: 'requested',
4935
- result: 'accepted',
4936
- authMethod: req.liveDeskLauncherShutdown === true
4937
- ? 'launcher-internal-shutdown-token'
4938
- : 'local-admin-session'
4939
- });
4940
4319
  runtimeManager.emit('runtime.shutdown.requested', { role: runtimeRole });
4941
4320
  res.json({ ok: true, shuttingDown: true, role: runtimeRole });
4942
4321
  setTimeout(() => {
@@ -4975,8 +4354,6 @@ app.post('/api/auth/session', async (req, res) => {
4975
4354
  const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
4976
4355
  try {
4977
4356
  const user = await verifySupabaseUser(accessToken);
4978
- runtimeSessionGeneration += 1;
4979
- runtimeRefreshPromise = null;
4980
4357
  runtimeAccessToken = accessToken;
4981
4358
  runtimeRefreshToken = refreshToken;
4982
4359
  runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
@@ -4993,26 +4370,9 @@ app.post('/api/auth/session', async (req, res) => {
4993
4370
  if (runtimeRole === 'hub') {
4994
4371
  startHubHostTargetLeaseRenewal();
4995
4372
  }
4996
- recordSecurityAudit({
4997
- action: 'auth.session.established',
4998
- phase: 'complete',
4999
- result: 'success',
5000
- actorAccountId: user.id,
5001
- actorUserId: user.id,
5002
- authMethod: 'supabase-access-refresh',
5003
- requestHash: requestAuditHash({ userId: user.id, expiresAt })
5004
- });
5005
4373
  res.json({ ok: true, authenticated: true, persisted, userId: user.id, role: runtimeRole, hostTarget });
5006
4374
  } catch (error) {
5007
4375
  const message = error instanceof Error ? error.message : String(error);
5008
- recordSecurityAudit({
5009
- action: 'auth.session.establish',
5010
- phase: 'complete',
5011
- result: 'rejected',
5012
- reason: message,
5013
- authMethod: 'supabase-access-refresh',
5014
- requestHash: requestAuditHash({ hasAccessToken: Boolean(req.body?.accessToken || req.body?.access_token), hasRefreshToken: Boolean(req.body?.refreshToken || req.body?.refresh_token) })
5015
- });
5016
4376
  const status = authVerificationHttpStatus(message);
5017
4377
  res.status(status).json({ ok: false, error: message });
5018
4378
  }
@@ -5308,30 +4668,11 @@ function startHubHostTargetLeaseRenewal() {
5308
4668
 
5309
4669
  app.delete('/api/auth/session', async (_req, res) => {
5310
4670
  noStore(res);
5311
- const actorUserId = runtimeManager.getSnapshot().userId || '';
5312
4671
  const hostTarget = runtimeRole === 'hub'
5313
4672
  ? await clearHubHostTarget('logout')
5314
4673
  : { ok: true, active: false };
5315
- const providerLogout = await revokeRuntimeProviderSession();
5316
4674
  clearRuntimeSession();
5317
- 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
- });
4675
+ res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
5335
4676
  });
5336
4677
 
5337
4678
  app.get('/api/hub/status', (_req, res) => {
@@ -5682,33 +5023,7 @@ app.post('/api/remote/files/from-hub', requireHubFeatureAccess, (req, res) => {
5682
5023
  const job = hubTransferJobs.create({
5683
5024
  itemIds,
5684
5025
  deviceIds,
5685
- remoteDirectory: req.body?.remoteDirectory,
5686
- onComplete: async ({ completed, job: completedJob }) => {
5687
- await recordSecurityAuditRequired({
5688
- action: 'remote.file.job',
5689
- phase: 'complete',
5690
- result: completed ? 'success' : 'failed',
5691
- reason: completed ? '' : completedJob?.error || 'file-transfer-failed',
5692
- deviceIds,
5693
- sessionId: completedJob?.jobId || '',
5694
- requestHash: requestAuditHash({
5695
- jobId: completedJob?.jobId || '',
5696
- deviceIds,
5697
- remoteDirectory: req.body?.remoteDirectory || ''
5698
- }),
5699
- authMethod: 'local-admin-session',
5700
- details: {
5701
- state: completedJob?.state || '',
5702
- totalFiles: Number(completedJob?.totalFiles || 0),
5703
- completedFiles: Number(completedJob?.completedFiles || 0),
5704
- totalBytes: Number(completedJob?.totalBytes || 0),
5705
- sentBytes: Number(completedJob?.sentBytes || 0),
5706
- failedTargets: Array.isArray(completedJob?.failedTargets)
5707
- ? completedJob.failedTargets.length
5708
- : 0
5709
- }
5710
- });
5711
- }
5026
+ remoteDirectory: req.body?.remoteDirectory
5712
5027
  });
5713
5028
  res.status(202).json({ ok: true, jobId: job.jobId, state: job.state });
5714
5029
  } catch (error) {
@@ -5790,80 +5105,56 @@ app.post('/api/remote/filesystem/shared-folders/:folderId/sync', requireHubFeatu
5790
5105
  }
5791
5106
  });
5792
5107
 
5793
- app.post('/api/remote/files/transfer', requireHubFeatureAccess, async (req, res) => {
5108
+ app.post('/api/remote/files/transfer', requireHubFeatureAccess, (req, res) => {
5794
5109
  noStore(res);
5795
5110
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5796
5111
  if (deviceIds.length === 0) {
5797
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5798
5112
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5799
5113
  return;
5800
5114
  }
5801
5115
  const normalized = normalizeTransferFiles(req.body?.files);
5802
5116
  if (!normalized.ok) {
5803
- recordSecurityAudit({ action: 'remote.file.transfer', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5804
5117
  res.status(400).json({ ok: false, error: normalized.error, totalBytes: normalized.totalBytes || 0 });
5805
5118
  return;
5806
5119
  }
5807
5120
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5808
5121
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5809
5122
  const queuedAt = new Date().toISOString();
5810
- 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
- }
5123
+ const results = deviceIds.map(deviceId => ({
5124
+ deviceId,
5125
+ ...remoteHub.sendCommand(deviceId, {
5126
+ command: 'file.transfer',
5127
+ payload: {
5128
+ transferId,
5129
+ remoteDirectory,
5130
+ files: normalized.files,
5131
+ totalBytes: normalized.totalBytes,
5132
+ requestedAt: queuedAt
5133
+ }
5134
+ })
5135
+ }));
5136
+ const queued = results.filter(result => result.ok).length;
5137
+ res.json({
5138
+ ok: queued > 0,
5139
+ transferId,
5140
+ queued,
5141
+ total: deviceIds.length,
5142
+ totalBytes: normalized.totalBytes,
5143
+ files: normalized.files.length,
5144
+ results,
5145
+ error: queued > 0 ? undefined : 'no-transfer-queued'
5146
+ });
5854
5147
  });
5855
5148
 
5856
- app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) => {
5149
+ app.post('/api/remote/files/chunk', requireHubFeatureAccess, (req, res) => {
5857
5150
  noStore(res);
5858
5151
  const deviceIds = normalizeDeviceIds(req.body?.deviceIds);
5859
5152
  if (deviceIds.length === 0) {
5860
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: 'no-target-devices', authMethod: 'local-admin-session' });
5861
5153
  res.status(400).json({ ok: false, error: 'no-target-devices' });
5862
5154
  return;
5863
5155
  }
5864
5156
  const normalized = normalizeTransferChunk(req.body || {});
5865
5157
  if (!normalized.ok) {
5866
- recordSecurityAudit({ action: 'remote.file.chunk', phase: 'complete', result: 'rejected', reason: normalized.error, deviceIds, authMethod: 'local-admin-session' });
5867
5158
  res.status(400).json({ ok: false, error: normalized.error, byteLength: normalized.byteLength || 0 });
5868
5159
  return;
5869
5160
  }
@@ -5871,50 +5162,30 @@ app.post('/api/remote/files/chunk', requireHubFeatureAccess, async (req, res) =>
5871
5162
  const transferId = String(req.body?.transferId || crypto.randomUUID()).replace(/[^a-zA-Z0-9_.:-]/g, '-').slice(0, 128);
5872
5163
  const remoteDirectory = String(req.body?.remoteDirectory || '').replace(/[\0\r\n\t]/g, ' ').trim().slice(0, 600);
5873
5164
  const queuedAt = new Date().toISOString();
5874
- 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
- }
5165
+ const results = deviceIds.map(deviceId => ({
5166
+ deviceId,
5167
+ ...remoteHub.sendCommand(deviceId, {
5168
+ command: 'file.transfer.chunk',
5169
+ payload: {
5170
+ transferId,
5171
+ remoteDirectory,
5172
+ ...normalized.chunk,
5173
+ requestedAt: queuedAt
5174
+ }
5175
+ })
5176
+ }));
5177
+ const queued = results.filter(result => result.ok).length;
5178
+ res.json({
5179
+ ok: queued > 0,
5180
+ transferId,
5181
+ queued,
5182
+ total: deviceIds.length,
5183
+ byteLength: normalized.chunk.byteLength,
5184
+ offset: normalized.chunk.offset,
5185
+ final: normalized.chunk.final,
5186
+ results,
5187
+ error: queued > 0 ? undefined : 'no-transfer-queued'
5188
+ });
5918
5189
  });
5919
5190
 
5920
5191
  const MANUAL_POWER_ACTIONS = new Set(['lock', 'sleep', 'restart', 'shutdown']);
@@ -6203,65 +5474,14 @@ if (existsSync(webIndexPath)) {
6203
5474
  });
6204
5475
  }
6205
5476
 
6206
- function isAuthorizedAdminWebSocket(req) {
6207
- if (!enforceLocalAdminAuth) return true;
6208
- const protocols = String(req.headers['sec-websocket-protocol'] || '')
6209
- .split(',')
6210
- .map(value => value.trim())
6211
- .filter(Boolean);
6212
- const adminToken = protocols.find(value => value.startsWith('livedesk-admin-'))?.slice('livedesk-admin-'.length) || '';
6213
- const csrfToken = protocols.find(value => value.startsWith('livedesk-admin-csrf-'))?.slice('livedesk-admin-csrf-'.length) || '';
6214
- return secureExactTestTokenMatches(adminToken, localAdminSessionToken)
6215
- && secureExactTestTokenMatches(csrfToken, localAdminCsrfToken);
6216
- }
6217
-
6218
5477
  httpServer.on('upgrade', (req, socket, head) => {
6219
5478
  if (!isTrustedBrowserRequest(req)) {
6220
5479
  socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
6221
5480
  socket.destroy();
6222
5481
  return;
6223
5482
  }
6224
- if (!isAuthorizedAdminWebSocket(req)) {
6225
- recordSecurityAudit({
6226
- action: 'local-admin.websocket',
6227
- phase: 'complete',
6228
- result: 'rejected',
6229
- reason: 'admin-session-required',
6230
- requestHash: requestAuditHash({ path: req.url || '' }),
6231
- authMethod: 'local-admin-websocket'
6232
- });
6233
- socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
6234
- socket.destroy();
6235
- return;
6236
- }
6237
- let parsed;
6238
5483
  try {
6239
- parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
6240
- } catch {
6241
- socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
6242
- socket.destroy();
6243
- return;
6244
- }
6245
- const websocketPath = parsed.pathname;
6246
- if (![
6247
- '/api/remote/frames/ws',
6248
- '/api/remote/atlas/ws',
6249
- '/api/remote/input/ws',
6250
- '/api/remote/audio/ws'
6251
- ].includes(websocketPath)) {
6252
- socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
6253
- socket.destroy();
6254
- return;
6255
- }
6256
- void recordSecurityAuditRequired({
6257
- action: 'local-admin.websocket',
6258
- phase: 'accepted',
6259
- result: 'accepted',
6260
- sessionId: localAdminSessionId,
6261
- requestHash: requestAuditHash({ path: websocketPath }),
6262
- authMethod: 'local-admin-websocket',
6263
- details: { path: websocketPath }
6264
- }).then(() => {
5484
+ const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
6265
5485
  if (parsed.pathname === '/api/remote/frames/ws') {
6266
5486
  frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
6267
5487
  return;
@@ -6278,10 +5498,12 @@ httpServer.on('upgrade', (req, socket, head) => {
6278
5498
  audioWss.handleUpgrade(req, socket, head, ws => audioWss.emit('connection', ws, req));
6279
5499
  return;
6280
5500
  }
6281
- }).catch(() => {
6282
- socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
5501
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
6283
5502
  socket.destroy();
6284
- });
5503
+ } catch {
5504
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
5505
+ socket.destroy();
5506
+ }
6285
5507
  });
6286
5508
 
6287
5509
  frameWss.on('connection', (ws, req) => {
@@ -6305,11 +5527,11 @@ frameWss.on('connection', (ws, req) => {
6305
5527
  ws.on('message', data => {
6306
5528
  try {
6307
5529
  const payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
6308
- if (payload?.type === 'subscribe') {
6309
- updateFrameSubscription(ws, payload);
6310
- } else if (payload?.type === 'refresh-live') {
6311
- refreshFrameSubscriptionLive(ws, payload);
6312
- } else if (payload?.type === 'restart-live') {
5530
+ if (payload?.type === 'subscribe') {
5531
+ updateFrameSubscription(ws, payload);
5532
+ } else if (payload?.type === 'refresh-live') {
5533
+ refreshFrameSubscriptionLive(ws, payload);
5534
+ } else if (payload?.type === 'restart-live') {
6313
5535
  restartFrameSubscriptionLive(ws, payload);
6314
5536
  }
6315
5537
  } catch {
@@ -6398,17 +5620,12 @@ inputWss.on('connection', ws => {
6398
5620
  inputClients.add(ws);
6399
5621
  ws.liveDeskInputClientId = `riws-${++inputClientSeq}`;
6400
5622
  ws.liveDeskInputDeviceIds = new Set();
6401
- ws.liveDeskInputAuditReady = false;
6402
5623
  try {
6403
5624
  ws._socket?.setNoDelay?.(true);
6404
5625
  } catch {
6405
5626
  // Best-effort latency hint for browser input sockets.
6406
5627
  }
6407
5628
  ws.on('message', data => {
6408
- if (ws.liveDeskInputAuditReady !== true) {
6409
- sendJson(ws, { type: 'RemoteInputError', error: 'security-audit-pending' });
6410
- return;
6411
- }
6412
5629
  let payload;
6413
5630
  try {
6414
5631
  payload = JSON.parse(Buffer.isBuffer(data) ? data.toString('utf8') : String(data || ''));
@@ -6449,43 +5666,19 @@ inputWss.on('connection', ws => {
6449
5666
  });
6450
5667
  });
6451
5668
  const releaseBrowserInputOwner = reason => {
6452
- if (ws.liveDeskInputReleased) return;
6453
- ws.liveDeskInputReleased = true;
6454
5669
  inputClients.delete(ws);
6455
5670
  for (const deviceId of ws.liveDeskInputDeviceIds || []) {
6456
5671
  remoteHub.releaseInputOwner(deviceId, ws.liveDeskInputClientId, reason);
6457
5672
  }
6458
- void recordSecurityAuditRequired({
6459
- action: 'remote.control.browser-session',
6460
- phase: 'complete',
6461
- result: 'success',
6462
- reason,
6463
- deviceIds: [...(ws.liveDeskInputDeviceIds || [])],
6464
- sessionId: ws.liveDeskInputClientId,
6465
- authMethod: 'local-admin-websocket'
6466
- }).catch(() => {
6467
- // The global fail-closed gate is marked by recordSecurityAuditRequired.
6468
- });
6469
5673
  ws.liveDeskInputDeviceIds?.clear?.();
6470
5674
  };
6471
5675
  ws.on('close', () => releaseBrowserInputOwner('browser-input-websocket-closed'));
6472
5676
  ws.on('error', () => releaseBrowserInputOwner('browser-input-websocket-error'));
6473
- 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');
5677
+ sendJson(ws, {
5678
+ type: 'RemoteInputSocketReady',
5679
+ protocol: 'livedesk.remote.input.json.v1',
5680
+ clientId: ws.liveDeskInputClientId,
5681
+ timestamp: new Date().toISOString()
6489
5682
  });
6490
5683
  });
6491
5684
 
@@ -6501,9 +5694,6 @@ httpServer.listen(httpPort, httpHost, () => {
6501
5694
  console.log(`[LiveDesk Hub] Version ${managerVersion}`);
6502
5695
  console.log(`[LiveDesk Hub] HTTP API http://${httpHost}:${httpPort}`);
6503
5696
  console.log(`[LiveDesk Hub] Client endpoint ${status.agentEndpoint} pair=${status.pairTokenPreview}`);
6504
- void restorePersistedHubSessionAtStartup().catch(error => {
6505
- console.warn(`[LiveDesk Hub] Saved sign-in startup restore deferred to the Hub page: ${error?.message || error}`);
6506
- });
6507
5697
  });
6508
5698
 
6509
5699
  const roleWatchTimer = runtimeRole === 'hub'
@@ -6585,7 +5775,6 @@ function shutdownHub(signal) {
6585
5775
  const startedAt = Date.now();
6586
5776
  console.log(`[LiveDesk Hub] Shutdown started signal=${signal} grace=${hubShutdownGraceMs}ms timeout=${hubShutdownTimeoutMs}ms.`);
6587
5777
  if (roleWatchTimer) clearInterval(roleWatchTimer);
6588
- clearInterval(captureRetentionTimer);
6589
5778
  clearInterval(browserWebSocketHeartbeatTimer);
6590
5779
  runSynchronousShutdownStep('update manager close', () => liveDeskUpdateManager?.close());
6591
5780
  atlasClients.clear();