@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/package.json +32 -32
- package/src/agents/agent-audit-store.js +6 -16
- package/src/agents/agent-permissions.js +27 -33
- package/src/agents/agent-tool-registry.js +322 -340
- package/src/captures/capture-store.js +252 -299
- package/src/filesystem/shared-folders.js +181 -189
- package/src/filesystem/transfer-jobs.js +314 -345
- package/src/live-desk-update.js +615 -683
- package/src/remote-hub.js +59 -643
- package/src/server.js +111 -957
- package/src/settings/settings-schema.js +40 -32
- package/src/settings/settings-store.js +2 -7
- package/src/transport/relay-hub-control.js +1376 -1703
- package/src/transport/udp-hub-transport.js +520 -543
- package/src/transport/udp-rendezvous.js +408 -574
- package/src/security/device-credential-authority.js +0 -406
- package/src/security/security-audit-store.js +0 -260
- package/src/transport/secure-direct-acceptor.js +0 -543
package/src/server.js
CHANGED
|
@@ -40,8 +40,7 @@ import { AgentRuntimeError } from './agents/agent-runtime-error.js';
|
|
|
40
40
|
import { AGENT_PERMISSION_MODES, createAgentPermissionPolicy, evaluateAgentToolPermission, hashAgentPermissionPolicy } from './agents/agent-permissions.js';
|
|
41
41
|
import { createAgentPermissionStore } from './agents/agent-permission-store.js';
|
|
42
42
|
import { createAgentAuditStore } from './agents/agent-audit-store.js';
|
|
43
|
-
import {
|
|
44
|
-
import { getAgentToolDefinition, isRetiredAgentMutatingToolName } from './agents/agent-tool-registry.js';
|
|
43
|
+
import { getAgentToolDefinition } from './agents/agent-tool-registry.js';
|
|
45
44
|
import { LiveDeskSettingsStore, SettingsConflictError } from './settings/settings-store.js';
|
|
46
45
|
import { effectiveDevicePolicy } from './settings/settings-schema.js';
|
|
47
46
|
import { buildEffectiveDevicePolicy } from './settings/effective-device-policy.js';
|
|
@@ -49,7 +48,6 @@ import { CaptureStore } from './captures/capture-store.js';
|
|
|
49
48
|
import { createLiveDeskUpdateManager } from './live-desk-update.js';
|
|
50
49
|
import { createHubUdpTransport } from './transport/udp-hub-transport.js';
|
|
51
50
|
import { normalizeRuntimeAuthSession, runtimeRoleError } from '../../runtime-core/src/index.js';
|
|
52
|
-
import { createOsSecretStore, OS_SECRET_REFERENCE } from '../../runtime-core/src/os-secret-store.js';
|
|
53
51
|
import { fetchAuthResponse, AUTH_REQUEST_TIMEOUT_MS } from '../../runtime-core/src/auth-http.js';
|
|
54
52
|
import { PRODUCTION_SUPABASE_PUBLISHABLE_KEY, PRODUCTION_SUPABASE_URL } from '../../runtime-core/src/auth-config.js';
|
|
55
53
|
import { createHubRuntime } from './runtime/hub-runtime.js';
|
|
@@ -67,18 +65,6 @@ const webIndexPath = resolve(webDistPath, 'index.html');
|
|
|
67
65
|
const packageInfo = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf8'));
|
|
68
66
|
const httpHost = process.env.LIVEDESK_HUB_HTTP_HOST || '127.0.0.1';
|
|
69
67
|
const httpPort = Number(process.env.LIVEDESK_HUB_HTTP_PORT || process.env.PORT || 5179);
|
|
70
|
-
if (!isLoopbackBindHost(httpHost) && process.env.LIVEDESK_TEST_MODE !== '1') {
|
|
71
|
-
throw new Error('hub-http-lan-bind-requires-tls-authenticated-proxy');
|
|
72
|
-
}
|
|
73
|
-
const localAdminSessionToken = crypto.randomBytes(32).toString('base64url');
|
|
74
|
-
const localAdminCsrfToken = crypto.randomBytes(32).toString('base64url');
|
|
75
|
-
const localAdminSessionId = crypto.randomUUID();
|
|
76
|
-
const launcherShutdownToken = /^[A-Za-z0-9_-]{43}$/.test(
|
|
77
|
-
String(process.env.LIVEDESK_LAUNCHER_SHUTDOWN_TOKEN || '').trim()
|
|
78
|
-
)
|
|
79
|
-
? String(process.env.LIVEDESK_LAUNCHER_SHUTDOWN_TOKEN).trim()
|
|
80
|
-
: '';
|
|
81
|
-
const enforceLocalAdminAuth = process.env.LIVEDESK_TEST_MODE !== '1';
|
|
82
68
|
const runtimeRole = String(process.env.LIVEDESK_RUNTIME_ROLE || 'hub').trim().toLowerCase() === 'client' ? 'client' : 'hub';
|
|
83
69
|
const runtimeDeviceId = String(process.env.LIVEDESK_DEVICE_ID || '').trim();
|
|
84
70
|
const runtimeDeviceName = String(process.env.LIVEDESK_DEVICE_NAME || os.hostname()).trim() || os.hostname();
|
|
@@ -182,18 +168,10 @@ const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
|
182
168
|
const runtimeAuthStatePath = process.env.LIVEDESK_DESKTOP_HOST === '1'
|
|
183
169
|
? ''
|
|
184
170
|
: String(process.env.LIVEDESK_AUTH_STATE_PATH || '').trim();
|
|
185
|
-
const runtimeRefreshSecretStore = createOsSecretStore({
|
|
186
|
-
service: 'LiveDesk',
|
|
187
|
-
account: 'client-refresh-token',
|
|
188
|
-
dataDir: process.env.LIVEDESK_STATE_DIR || (runtimeAuthStatePath ? dirname(runtimeAuthStatePath) : undefined)
|
|
189
|
-
});
|
|
190
171
|
const testLicensePlan = process.env.LIVEDESK_TEST_MODE === '1'
|
|
191
172
|
&& ['ltd', 'pro'].includes(String(process.env.LIVEDESK_TEST_LICENSE_PLAN || '').toLowerCase())
|
|
192
173
|
? String(process.env.LIVEDESK_TEST_LICENSE_PLAN).toLowerCase()
|
|
193
174
|
: '';
|
|
194
|
-
const testSecurityAccountId = process.env.LIVEDESK_TEST_MODE === '1'
|
|
195
|
-
? String(process.env.LIVEDESK_ACCOUNT_ID || '').trim().slice(0, 128)
|
|
196
|
-
: '';
|
|
197
175
|
const persistentSessionGcEnabled =
|
|
198
176
|
process.env.LIVEDESK_TEST_MODE === '1'
|
|
199
177
|
&& process.env.LIVEDESK_PERSISTENT_SESSION_TEST_GC === '1';
|
|
@@ -207,8 +185,6 @@ let connectedDeviceCount = 0;
|
|
|
207
185
|
let runtimeAccessToken = '';
|
|
208
186
|
let runtimeRefreshToken = '';
|
|
209
187
|
let runtimeAccessTokenExpiresAt = 0;
|
|
210
|
-
let runtimeSessionGeneration = 0;
|
|
211
|
-
let runtimeRefreshPromise = null;
|
|
212
188
|
let roleWatchInFlight = false;
|
|
213
189
|
let verifiedLicense = {
|
|
214
190
|
userId: '',
|
|
@@ -222,8 +198,6 @@ let inputClientSeq = 0;
|
|
|
222
198
|
let audioClientSeq = 0;
|
|
223
199
|
let liveDeskUpdateManager = null;
|
|
224
200
|
let hubTransferJobs = null;
|
|
225
|
-
let securityAuditStore = null;
|
|
226
|
-
let securityAuditHealthy = true;
|
|
227
201
|
const HUB_HOST_TARGET_LEASE_MS = Math.max(5000, readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_LEASE_MS', 60_000));
|
|
228
202
|
const HUB_HOST_TARGET_RENEW_MS = Math.max(1000, Math.min(
|
|
229
203
|
readPositiveIntegerEnv('LIVEDESK_HOST_TARGET_RENEW_MS', 20_000),
|
|
@@ -270,39 +244,6 @@ function secureExactTestTokenMatches(actual, expected) {
|
|
|
270
244
|
function handleRemoteHubEvent(type, event) {
|
|
271
245
|
liveDeskUpdateManager?.handleRemoteEvent(type, event);
|
|
272
246
|
hubTransferJobs?.handleRemoteEvent(type, event);
|
|
273
|
-
const auditedRemoteActions = {
|
|
274
|
-
RemoteSecurityHandshakeAudit: event?.action || 'remote.handshake',
|
|
275
|
-
RemoteSecurityPolicyAudit: 'remote.connection-policy',
|
|
276
|
-
RemoteAbuseDefense: 'remote.abuse-defense',
|
|
277
|
-
RemoteDeviceConnected: 'remote.device.connected',
|
|
278
|
-
RemoteDeviceDisconnected: 'remote.device.disconnected',
|
|
279
|
-
RemoteInputSocketConnected: 'remote.control.channel-started',
|
|
280
|
-
RemoteInputSocketDisconnected: 'remote.control.channel-ended',
|
|
281
|
-
RemoteControlSessionStarted: 'remote.control.session-started',
|
|
282
|
-
RemoteControlSessionEnded: 'remote.control.session-ended',
|
|
283
|
-
RemoteFileSocketConnected: 'remote.file.channel-started',
|
|
284
|
-
RemoteFileSocketDisconnected: 'remote.file.channel-ended',
|
|
285
|
-
RemoteEnrollmentTokenRotated: 'remote.enrollment-token.rotated'
|
|
286
|
-
};
|
|
287
|
-
if (auditedRemoteActions[type]) {
|
|
288
|
-
const device = event?.device || {};
|
|
289
|
-
recordSecurityAudit({
|
|
290
|
-
action: auditedRemoteActions[type],
|
|
291
|
-
phase: /Disconnected|ended|rotated/i.test(`${type}:${auditedRemoteActions[type]}`) ? 'complete' : 'start',
|
|
292
|
-
result: event?.result || (event?.reason && /RemoteSecurity(?:Handshake|Policy)Audit|RemoteAbuseDefense/.test(type) ? 'rejected' : 'success'),
|
|
293
|
-
reason: event?.reason || '',
|
|
294
|
-
actorAccountId: event?.accountId || '',
|
|
295
|
-
hubId: event?.hubId || event?.remoteHub?.hostInstanceId || '',
|
|
296
|
-
deviceId: event?.deviceId || device.deviceId || '',
|
|
297
|
-
sessionId: event?.sessionId || device.sessionId || '',
|
|
298
|
-
authMethod: /RemoteSecurity(?:Handshake|Policy)Audit|RemoteAbuseDefense/.test(type) ? 'device-credential-p256' : 'authenticated-device-session',
|
|
299
|
-
details: {
|
|
300
|
-
transport: event?.transport || device.transport || '',
|
|
301
|
-
channel: event?.channel || '',
|
|
302
|
-
credentialSerial: event?.credentialSerial || ''
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
247
|
if (traceRemoteTestEventsEnabled
|
|
307
248
|
&& (type === 'RemoteFrameDropped' || type === 'RemoteFrameTransportProofAccepted')) {
|
|
308
249
|
console.warn(`[LiveDesk Hub Test Event] ${type} ${JSON.stringify({
|
|
@@ -352,12 +293,6 @@ function handleRemoteHubEvent(type, event) {
|
|
|
352
293
|
if (type === 'RemoteDeviceConnected' || type === 'RemoteDeviceDisconnected') {
|
|
353
294
|
connectedDeviceCount = Number(remoteHub.getStatus({ includeSecrets: false }).connectedDeviceCount || 0);
|
|
354
295
|
}
|
|
355
|
-
if (type === 'RemoteEnrollmentTokenRotated') {
|
|
356
|
-
void publishHubHostTargetWithPendingRoleTakeover('enrollment-token-rotated').catch(error => {
|
|
357
|
-
console.warn(`[LiveDesk Hub] Enrollment token publication failed: ${error?.message || error}`);
|
|
358
|
-
});
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
296
|
if (type !== 'RemoteDeviceConnected') {
|
|
362
297
|
return;
|
|
363
298
|
}
|
|
@@ -494,19 +429,7 @@ function readPersistedRuntimeSession(state = readRuntimeAuthState()) {
|
|
|
494
429
|
try {
|
|
495
430
|
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
496
431
|
const session = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
497
|
-
|
|
498
|
-
const plaintextRefreshToken = String(session.refresh_token || '').trim();
|
|
499
|
-
const refreshToken = plaintextRefreshToken || (session.refresh_token_ref === OS_SECRET_REFERENCE
|
|
500
|
-
? runtimeRefreshSecretStore.read()
|
|
501
|
-
: '');
|
|
502
|
-
if (plaintextRefreshToken) {
|
|
503
|
-
if (!runtimeRefreshSecretStore.write(plaintextRefreshToken)) return null;
|
|
504
|
-
const migrated = { ...session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
505
|
-
delete migrated.refresh_token;
|
|
506
|
-
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(migrated);
|
|
507
|
-
writePrivateRuntimeAuthState(state);
|
|
508
|
-
}
|
|
509
|
-
return { ...session, refresh_token: refreshToken };
|
|
432
|
+
return session && typeof session === 'object' ? session : null;
|
|
510
433
|
} catch {
|
|
511
434
|
return null;
|
|
512
435
|
}
|
|
@@ -550,10 +473,7 @@ function persistRuntimeSession(session) {
|
|
|
550
473
|
}
|
|
551
474
|
}, { requireRefreshToken: true });
|
|
552
475
|
if (!normalized.ok) return false;
|
|
553
|
-
|
|
554
|
-
const persisted = { ...normalized.session, refresh_token_ref: OS_SECRET_REFERENCE };
|
|
555
|
-
delete persisted.refresh_token;
|
|
556
|
-
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(persisted);
|
|
476
|
+
state[CLIENT_AUTH_STORAGE_KEY] = JSON.stringify(normalized.session);
|
|
557
477
|
writePrivateRuntimeAuthState(state);
|
|
558
478
|
return true;
|
|
559
479
|
}
|
|
@@ -562,13 +482,10 @@ function clearPersistedRuntimeSession() {
|
|
|
562
482
|
if (!runtimeAuthStatePath) return false;
|
|
563
483
|
const state = readRuntimeAuthState();
|
|
564
484
|
delete state[CLIENT_AUTH_STORAGE_KEY];
|
|
565
|
-
runtimeRefreshSecretStore.clear();
|
|
566
485
|
return writePrivateRuntimeAuthState(state);
|
|
567
486
|
}
|
|
568
487
|
|
|
569
488
|
function clearRuntimeSession() {
|
|
570
|
-
runtimeSessionGeneration += 1;
|
|
571
|
-
runtimeRefreshPromise = null;
|
|
572
489
|
runtimeAccessToken = '';
|
|
573
490
|
runtimeRefreshToken = '';
|
|
574
491
|
runtimeAccessTokenExpiresAt = 0;
|
|
@@ -589,137 +506,46 @@ async function getRuntimeAccessToken() {
|
|
|
589
506
|
return accessToken;
|
|
590
507
|
}
|
|
591
508
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
headers: {
|
|
602
|
-
apikey: supabasePublishableKey,
|
|
603
|
-
'Content-Type': 'application/json',
|
|
604
|
-
Accept: 'application/json'
|
|
605
|
-
},
|
|
606
|
-
body: JSON.stringify({ refresh_token: refreshToken })
|
|
509
|
+
const response = await fetchAuthResponse(
|
|
510
|
+
fetch,
|
|
511
|
+
`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`,
|
|
512
|
+
{
|
|
513
|
+
method: 'POST',
|
|
514
|
+
headers: {
|
|
515
|
+
apikey: supabasePublishableKey,
|
|
516
|
+
'Content-Type': 'application/json',
|
|
517
|
+
Accept: 'application/json'
|
|
607
518
|
},
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
throw new Error(`hub-session-refresh-provider-failed:${response.status}`);
|
|
618
|
-
}
|
|
619
|
-
const refreshed = await response.json().catch(() => null);
|
|
620
|
-
const nextAccessToken = String(refreshed?.access_token || '').trim();
|
|
621
|
-
if (!nextAccessToken) {
|
|
622
|
-
throw new Error('hub-session-refresh-missing-access-token');
|
|
623
|
-
}
|
|
624
|
-
if (runtimeSessionGeneration !== ownerGeneration || runtimeRefreshToken !== refreshToken) {
|
|
625
|
-
throw new Error('hub-session-refresh-owner-retired');
|
|
626
|
-
}
|
|
627
|
-
runtimeAccessToken = nextAccessToken;
|
|
628
|
-
runtimeRefreshToken = String(refreshed?.refresh_token || refreshToken).trim();
|
|
629
|
-
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(refreshed?.expires_at)
|
|
630
|
-
|| (Number(refreshed?.expires_in) > 0 ? Date.now() + Number(refreshed.expires_in) * 1000 : 0);
|
|
631
|
-
try {
|
|
632
|
-
persistRuntimeSession({
|
|
633
|
-
access_token: runtimeAccessToken,
|
|
634
|
-
refresh_token: runtimeRefreshToken,
|
|
635
|
-
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
636
|
-
});
|
|
637
|
-
} catch (error) {
|
|
638
|
-
console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
519
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
520
|
+
},
|
|
521
|
+
SUPABASE_AUTH_TIMEOUT_MS
|
|
522
|
+
);
|
|
523
|
+
if (!response.ok) {
|
|
524
|
+
if (response.status === 400 || response.status === 401 || response.status === 403) {
|
|
525
|
+
clearRuntimeSession();
|
|
526
|
+
throw new Error(`hub-session-refresh-failed:${response.status}`);
|
|
639
527
|
}
|
|
640
|
-
|
|
641
|
-
})();
|
|
642
|
-
runtimeRefreshPromise = refreshOperation;
|
|
643
|
-
try {
|
|
644
|
-
return await refreshOperation;
|
|
645
|
-
} finally {
|
|
646
|
-
if (runtimeRefreshPromise === refreshOperation) runtimeRefreshPromise = null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async function restorePersistedHubSessionAtStartup() {
|
|
651
|
-
if (runtimeRole !== 'hub' || runtimeManager.getSnapshot().authenticated) {
|
|
652
|
-
return { ok: true, skipped: true, reason: 'not-required' };
|
|
528
|
+
throw new Error(`hub-session-refresh-provider-failed:${response.status}`);
|
|
653
529
|
}
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return { ok: false, skipped: true, reason: normalized.ok ? 'saved-user-missing' : normalized.error };
|
|
659
|
-
}
|
|
660
|
-
runtimeSessionGeneration += 1;
|
|
661
|
-
runtimeRefreshPromise = null;
|
|
662
|
-
runtimeAccessToken = String(normalized.session.access_token || '').trim();
|
|
663
|
-
runtimeRefreshToken = String(normalized.session.refresh_token || '').trim();
|
|
664
|
-
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(normalized.session.expires_at);
|
|
665
|
-
const accessToken = await getRuntimeAccessToken();
|
|
666
|
-
if (!accessToken) throw new Error('saved-hub-session-access-token-unavailable');
|
|
667
|
-
runtimeManager.setAuthenticated(true, userId);
|
|
668
|
-
persistRuntimeSession({
|
|
669
|
-
access_token: runtimeAccessToken,
|
|
670
|
-
refresh_token: runtimeRefreshToken,
|
|
671
|
-
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000),
|
|
672
|
-
user: normalized.session.user
|
|
673
|
-
});
|
|
674
|
-
const hostTarget = await publishHubHostTargetWithPendingRoleTakeover('saved-session-restored');
|
|
675
|
-
startHubHostTargetLeaseRenewal();
|
|
676
|
-
console.log(`[LiveDesk Hub] Saved sign-in restored locally; host target publication ${hostTarget?.ok === false ? 'will retry' : 'is active'}.`);
|
|
677
|
-
return { ok: true, skipped: false, hostTarget };
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function isLoopbackBindHost(value) {
|
|
681
|
-
const host = String(value || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
682
|
-
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
async function revokeRuntimeProviderSession() {
|
|
686
|
-
const hasRuntimeSession = Boolean(runtimeAccessToken || runtimeRefreshToken);
|
|
687
|
-
if (!hasRuntimeSession) return { ok: true, skipped: true, reason: 'no-runtime-session' };
|
|
688
|
-
|
|
689
|
-
let accessToken = String(runtimeAccessToken || '').trim();
|
|
690
|
-
try {
|
|
691
|
-
accessToken = await getRuntimeAccessToken() || accessToken;
|
|
692
|
-
} catch (error) {
|
|
693
|
-
const message = String(error?.message || error);
|
|
694
|
-
if (/^hub-session-refresh-failed:(?:400|401|403)$/.test(message)) {
|
|
695
|
-
return { ok: true, alreadyInvalid: true };
|
|
696
|
-
}
|
|
697
|
-
return { ok: false, error: message };
|
|
530
|
+
const refreshed = await response.json().catch(() => null);
|
|
531
|
+
const nextAccessToken = String(refreshed?.access_token || '').trim();
|
|
532
|
+
if (!nextAccessToken) {
|
|
533
|
+
throw new Error('hub-session-refresh-missing-access-token');
|
|
698
534
|
}
|
|
699
|
-
|
|
700
|
-
|
|
535
|
+
runtimeAccessToken = nextAccessToken;
|
|
536
|
+
runtimeRefreshToken = String(refreshed?.refresh_token || refreshToken).trim();
|
|
537
|
+
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(refreshed?.expires_at)
|
|
538
|
+
|| (Number(refreshed?.expires_in) > 0 ? Date.now() + Number(refreshed.expires_in) * 1000 : 0);
|
|
701
539
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
headers: {
|
|
708
|
-
apikey: supabasePublishableKey,
|
|
709
|
-
Authorization: `Bearer ${accessToken}`,
|
|
710
|
-
Accept: 'application/json'
|
|
711
|
-
}
|
|
712
|
-
},
|
|
713
|
-
SUPABASE_AUTH_TIMEOUT_MS
|
|
714
|
-
);
|
|
715
|
-
if (response.ok) return { ok: true, revoked: true };
|
|
716
|
-
if ([401, 403, 404].includes(response.status)) {
|
|
717
|
-
return { ok: true, alreadyInvalid: true, status: response.status };
|
|
718
|
-
}
|
|
719
|
-
return { ok: false, error: `provider-session-revoke-failed:${response.status}` };
|
|
540
|
+
persistRuntimeSession({
|
|
541
|
+
access_token: runtimeAccessToken,
|
|
542
|
+
refresh_token: runtimeRefreshToken,
|
|
543
|
+
expires_at: Math.floor(runtimeAccessTokenExpiresAt / 1000)
|
|
544
|
+
});
|
|
720
545
|
} catch (error) {
|
|
721
|
-
|
|
546
|
+
console.warn(`[LiveDesk Hub] Refreshed session persistence failed: ${error?.message || error}`);
|
|
722
547
|
}
|
|
548
|
+
return runtimeAccessToken;
|
|
723
549
|
}
|
|
724
550
|
|
|
725
551
|
async function verifySupabaseUser(accessToken) {
|
|
@@ -834,80 +660,14 @@ function requireHubFeatureAccess(_req, res, next) {
|
|
|
834
660
|
}
|
|
835
661
|
|
|
836
662
|
const agentDataDir = process.env.LIVEDESK_DATA_DIR || undefined;
|
|
837
|
-
securityAuditStore = createSecurityAuditStore({ dataDir: agentDataDir });
|
|
838
|
-
|
|
839
|
-
function requestAuditHash(value) {
|
|
840
|
-
try {
|
|
841
|
-
return crypto.createHash('sha256').update(JSON.stringify(value ?? {}), 'utf8').digest('base64url');
|
|
842
|
-
} catch {
|
|
843
|
-
return crypto.createHash('sha256').update('[unserializable]', 'utf8').digest('base64url');
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function securityAuditEvent(event = {}) {
|
|
848
|
-
const runtime = runtimeManager.getSnapshot();
|
|
849
|
-
return {
|
|
850
|
-
actorAccountId: event.actorAccountId || runtime.userId || '',
|
|
851
|
-
actorUserId: event.actorUserId || runtime.userId || '',
|
|
852
|
-
hubId: event.hubId || runtime.deviceId || '',
|
|
853
|
-
...event
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function markSecurityAuditFailed(error) {
|
|
858
|
-
if (securityAuditHealthy) {
|
|
859
|
-
securityAuditHealthy = false;
|
|
860
|
-
console.error(`[LiveDesk Hub] SECURITY AUDIT FAILURE: ${error?.message || error}`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
async function recordSecurityAuditRequired(event = {}) {
|
|
865
|
-
if (!securityAuditStore || !securityAuditHealthy) {
|
|
866
|
-
const error = new Error('security-audit-unavailable');
|
|
867
|
-
error.code = 'security-audit-unavailable';
|
|
868
|
-
throw error;
|
|
869
|
-
}
|
|
870
|
-
try {
|
|
871
|
-
return await securityAuditStore.record(securityAuditEvent(event));
|
|
872
|
-
} catch (error) {
|
|
873
|
-
markSecurityAuditFailed(error);
|
|
874
|
-
throw error;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function recordSecurityAudit(event = {}) {
|
|
879
|
-
void recordSecurityAuditRequired(event).catch(() => {
|
|
880
|
-
// Required mutation paths await recordSecurityAuditRequired directly.
|
|
881
|
-
// Best-effort lifecycle telemetry still marks the global audit gate failed.
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
|
|
885
663
|
const liveDeskSettingsStore = new LiveDeskSettingsStore({ dataDir: agentDataDir });
|
|
886
|
-
const captureStore = new CaptureStore({
|
|
887
|
-
dataDir: agentDataDir,
|
|
888
|
-
getRetentionPolicy: () => liveDeskSettingsStore.getCached()?.filesAudio?.captureAutoDelete || 'never',
|
|
889
|
-
onRetentionDelete: result => recordSecurityAudit({
|
|
890
|
-
action: 'privacy.capture.retention-delete',
|
|
891
|
-
phase: 'complete',
|
|
892
|
-
result: 'success',
|
|
893
|
-
authMethod: 'settings-retention-policy',
|
|
894
|
-
details: result
|
|
895
|
-
})
|
|
896
|
-
});
|
|
664
|
+
const captureStore = new CaptureStore({ dataDir: agentDataDir });
|
|
897
665
|
void captureStore.initialize().catch(error => {
|
|
898
666
|
console.warn(`[LiveDesk Hub] capture store initialization failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
899
667
|
});
|
|
900
|
-
void liveDeskSettingsStore.getRecord()
|
|
901
|
-
.
|
|
902
|
-
|
|
903
|
-
console.warn(`[LiveDesk Hub] settings or capture retention load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
904
|
-
});
|
|
905
|
-
const captureRetentionTimer = setInterval(() => {
|
|
906
|
-
void captureStore.enforceRetention().catch(error => {
|
|
907
|
-
console.warn(`[LiveDesk Hub] capture retention failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
908
|
-
});
|
|
909
|
-
}, 60 * 60_000);
|
|
910
|
-
captureRetentionTimer.unref?.();
|
|
668
|
+
void liveDeskSettingsStore.getRecord().catch(error => {
|
|
669
|
+
console.warn(`[LiveDesk Hub] settings load failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
670
|
+
});
|
|
911
671
|
|
|
912
672
|
const udpTransport = createHubUdpTransport({
|
|
913
673
|
env: process.env,
|
|
@@ -918,7 +678,6 @@ const udpTransport = createHubUdpTransport({
|
|
|
918
678
|
const remoteHub = createRemoteHub({
|
|
919
679
|
managerPackage: '@livedesk/hub',
|
|
920
680
|
managerVersion: packageInfo.version,
|
|
921
|
-
dataDir: agentDataDir,
|
|
922
681
|
env: {
|
|
923
682
|
...process.env,
|
|
924
683
|
MINDEXEC_MANAGER_PACKAGE: '@livedesk/hub',
|
|
@@ -930,10 +689,6 @@ const remoteHub = createRemoteHub({
|
|
|
930
689
|
emitEvent: handleRemoteHubEvent,
|
|
931
690
|
emitFrame: broadcastRemoteBinaryFrame,
|
|
932
691
|
emitAudio: broadcastRemoteBinaryAudio,
|
|
933
|
-
getSecurityIdentity: () => ({
|
|
934
|
-
accountId: runtimeManager.getSnapshot().userId || testSecurityAccountId,
|
|
935
|
-
hubId: remoteHub?.getSecurityStatus?.().hubId || ''
|
|
936
|
-
}),
|
|
937
692
|
getEffectiveDevicePolicy: ({ deviceId, capabilities }) => buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities }),
|
|
938
693
|
getWelcomeDevicePolicy: ({ deviceId, capabilities }) => {
|
|
939
694
|
const policy = buildEffectiveDevicePolicy(liveDeskSettingsStore.getCached(), { deviceId, capabilities });
|
|
@@ -1038,8 +793,6 @@ if (process.env.LIVEDESK_DESKTOP_HOST !== '1') {
|
|
|
1038
793
|
remoteHub,
|
|
1039
794
|
currentManagerVersion: process.env.LIVEDESK_MANAGER_VERSION || packageInfo.version,
|
|
1040
795
|
currentClientVersion: process.env.LIVEDESK_CLIENT_PACKAGE_VERSION || '',
|
|
1041
|
-
updateManifestUrl: process.env.LIVEDESK_UPDATE_MANIFEST_URL || '',
|
|
1042
|
-
updatePublicKey: process.env.LIVEDESK_UPDATE_PUBLIC_KEY || '',
|
|
1043
796
|
restartSupported: !!String(process.env.LIVEDESK_HUB_UPDATE_REQUEST_PATH || '').trim(),
|
|
1044
797
|
requestHubRestart
|
|
1045
798
|
});
|
|
@@ -1367,21 +1120,6 @@ async function dispatchAgentMcpTool(session, name, args = {}) {
|
|
|
1367
1120
|
return { ok: false, error: 'codex-tool-limit-reached' };
|
|
1368
1121
|
}
|
|
1369
1122
|
session.toolCallCount += 1;
|
|
1370
|
-
if (isRetiredAgentMutatingToolName(name)) {
|
|
1371
|
-
recordAgentAudit({
|
|
1372
|
-
event: 'tool-rejected',
|
|
1373
|
-
runId: session.runId,
|
|
1374
|
-
deviceIds: [],
|
|
1375
|
-
toolName: name,
|
|
1376
|
-
category: 'mutation',
|
|
1377
|
-
decision: 'deny',
|
|
1378
|
-
status: 'rejected',
|
|
1379
|
-
permissionMode: session.permissionPolicy?.mode,
|
|
1380
|
-
policyHash: session.permissionPolicyHash,
|
|
1381
|
-
details: { reason: 'agent-mutating-tool-disabled' }
|
|
1382
|
-
});
|
|
1383
|
-
return { ok: false, error: 'agent-mutating-tool-disabled' };
|
|
1384
|
-
}
|
|
1385
1123
|
const tool = getAgentToolDefinition(name);
|
|
1386
1124
|
if (!tool) return { ok: false, error: 'codex-tool-not-allowed' };
|
|
1387
1125
|
const validationError = validateAgentMcpArguments(name, args);
|
|
@@ -1574,18 +1312,11 @@ async function synchronizeAgentEnablement() {
|
|
|
1574
1312
|
}
|
|
1575
1313
|
|
|
1576
1314
|
const hubFilesystem = createHubFilesystem();
|
|
1577
|
-
const fileTransferCommandAckTimeoutMs = readPositiveIntegerEnv(
|
|
1578
|
-
'LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS',
|
|
1579
|
-
30_000
|
|
1580
|
-
);
|
|
1581
1315
|
hubTransferJobs = createHubTransferJobs({
|
|
1582
1316
|
filesystem: hubFilesystem,
|
|
1583
1317
|
remoteHub,
|
|
1584
1318
|
maxConcurrent: Number(process.env.LIVEDESK_MAX_FILE_JOBS || 2),
|
|
1585
|
-
commandResultTimeoutMs:
|
|
1586
|
-
getMaxFileSizeBytes: () => Number(
|
|
1587
|
-
liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
|
|
1588
|
-
|| 1024 * 1024 * 1024)
|
|
1319
|
+
commandResultTimeoutMs: readPositiveIntegerEnv('LIVEDESK_FILE_CHUNK_ACK_TIMEOUT_MS', 30_000)
|
|
1589
1320
|
});
|
|
1590
1321
|
const hubSharedFolders = createHubSharedFolders({
|
|
1591
1322
|
filesystem: hubFilesystem,
|
|
@@ -1706,7 +1437,7 @@ app.use((req, res, next) => {
|
|
|
1706
1437
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1707
1438
|
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
1708
1439
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
1709
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization
|
|
1440
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1710
1441
|
res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
|
|
1711
1442
|
}
|
|
1712
1443
|
if (req.method === 'OPTIONS') {
|
|
@@ -1715,143 +1446,7 @@ app.use((req, res, next) => {
|
|
|
1715
1446
|
}
|
|
1716
1447
|
next();
|
|
1717
1448
|
});
|
|
1718
|
-
|
|
1719
|
-
function parseHubJsonRequest(req, res, next, onParsed = next) {
|
|
1720
|
-
parseHubJsonBody(req, res, error => {
|
|
1721
|
-
if (!error) {
|
|
1722
|
-
onParsed();
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
const type = String(error?.type || '');
|
|
1726
|
-
const code = String(error?.code || '');
|
|
1727
|
-
const bodyError = type === 'entity.parse.failed'
|
|
1728
|
-
? { status: 400, error: 'invalid-json' }
|
|
1729
|
-
: type === 'entity.too.large'
|
|
1730
|
-
? { status: 413, error: 'request-too-large' }
|
|
1731
|
-
: type === 'request.aborted' || type === 'stream.not.readable' || code === 'ECONNABORTED'
|
|
1732
|
-
? { status: 400, error: 'request-body-unavailable' }
|
|
1733
|
-
: null;
|
|
1734
|
-
if (!bodyError) {
|
|
1735
|
-
next(error);
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
if (res.headersSent || res.destroyed || req.destroyed) return;
|
|
1739
|
-
res.status(bodyError.status).json({ ok: false, error: bodyError.error });
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
app.use((req, res, next) => {
|
|
1743
|
-
const mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
|
|
1744
|
-
if (!enforceLocalAdminAuth || req.path === '/api/security/session' || req.method === 'OPTIONS') {
|
|
1745
|
-
if (mutating) parseHubJsonRequest(req, res, next);
|
|
1746
|
-
else next();
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
const authorizedLauncherShutdown = req.method === 'POST'
|
|
1750
|
-
&& req.path === '/api/runtime/shutdown'
|
|
1751
|
-
&& isLoopbackAddress(req.socket?.remoteAddress)
|
|
1752
|
-
&& launcherShutdownToken
|
|
1753
|
-
&& secureExactTestTokenMatches(
|
|
1754
|
-
String(req.headers['x-livedesk-launcher-shutdown'] || ''),
|
|
1755
|
-
launcherShutdownToken
|
|
1756
|
-
);
|
|
1757
|
-
if (authorizedLauncherShutdown) {
|
|
1758
|
-
req.liveDeskLauncherShutdown = true;
|
|
1759
|
-
parseHubJsonRequest(req, res, next);
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const sensitiveRead = req.method === 'GET'
|
|
1763
|
-
&& ['/api/security/audit', '/api/security/rendezvous-issuer', '/api/privacy/inventory'].includes(req.path);
|
|
1764
|
-
if (!mutating && !sensitiveRead) {
|
|
1765
|
-
next();
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
const adminToken = String(req.headers['x-livedesk-admin-session'] || '');
|
|
1769
|
-
const csrfToken = String(req.headers['x-livedesk-admin-csrf'] || '');
|
|
1770
|
-
if (!secureExactTestTokenMatches(adminToken, localAdminSessionToken)) {
|
|
1771
|
-
recordSecurityAudit({
|
|
1772
|
-
action: 'local-admin.request',
|
|
1773
|
-
phase: 'complete',
|
|
1774
|
-
result: 'rejected',
|
|
1775
|
-
reason: 'admin-session-required',
|
|
1776
|
-
requestHash: requestAuditHash({ method: req.method, path: req.path }),
|
|
1777
|
-
authMethod: 'local-admin-session'
|
|
1778
|
-
});
|
|
1779
|
-
res.status(401).json({ ok: false, error: 'local-admin-session-required' });
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
if (mutating && !secureExactTestTokenMatches(csrfToken, localAdminCsrfToken)) {
|
|
1783
|
-
recordSecurityAudit({
|
|
1784
|
-
action: 'local-admin.request',
|
|
1785
|
-
phase: 'complete',
|
|
1786
|
-
result: 'rejected',
|
|
1787
|
-
reason: 'csrf-token-required',
|
|
1788
|
-
requestHash: requestAuditHash({ method: req.method, path: req.path }),
|
|
1789
|
-
authMethod: 'local-admin-session'
|
|
1790
|
-
});
|
|
1791
|
-
res.status(403).json({ ok: false, error: 'csrf-token-required' });
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
if (!securityAuditHealthy) {
|
|
1795
|
-
res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
req.liveDeskAdminSessionId = localAdminSessionId;
|
|
1799
|
-
if (!mutating) {
|
|
1800
|
-
next();
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
const beginMutationAudit = () => {
|
|
1805
|
-
const mutationRequestHash = requestAuditHash({ method: req.method, path: req.path });
|
|
1806
|
-
void recordSecurityAuditRequired({
|
|
1807
|
-
action: 'local-admin.mutation',
|
|
1808
|
-
phase: 'accepted',
|
|
1809
|
-
result: 'accepted',
|
|
1810
|
-
sessionId: localAdminSessionId,
|
|
1811
|
-
requestHash: mutationRequestHash,
|
|
1812
|
-
authMethod: 'local-admin-session',
|
|
1813
|
-
details: { method: req.method, path: req.path }
|
|
1814
|
-
}).then(() => {
|
|
1815
|
-
const originalEnd = res.end.bind(res);
|
|
1816
|
-
let completionStarted = false;
|
|
1817
|
-
res.end = (...args) => {
|
|
1818
|
-
if (completionStarted) return res;
|
|
1819
|
-
completionStarted = true;
|
|
1820
|
-
const statusCode = Number(res.statusCode || 200);
|
|
1821
|
-
void recordSecurityAuditRequired({
|
|
1822
|
-
action: 'local-admin.mutation',
|
|
1823
|
-
phase: 'complete',
|
|
1824
|
-
result: statusCode >= 200 && statusCode < 400 ? 'success' : 'rejected',
|
|
1825
|
-
reason: statusCode >= 400 ? `http-${statusCode}` : '',
|
|
1826
|
-
sessionId: localAdminSessionId,
|
|
1827
|
-
requestHash: mutationRequestHash,
|
|
1828
|
-
authMethod: 'local-admin-session',
|
|
1829
|
-
details: { method: req.method, path: req.path, statusCode }
|
|
1830
|
-
}).then(() => {
|
|
1831
|
-
originalEnd(...args);
|
|
1832
|
-
}).catch(error => {
|
|
1833
|
-
if (res.headersSent) {
|
|
1834
|
-
res.destroy(error);
|
|
1835
|
-
return;
|
|
1836
|
-
}
|
|
1837
|
-
const callback = [...args].reverse().find(value => typeof value === 'function');
|
|
1838
|
-
const body = JSON.stringify({ ok: false, error: 'security-audit-unavailable' });
|
|
1839
|
-
res.statusCode = 503;
|
|
1840
|
-
res.removeHeader('Content-Length');
|
|
1841
|
-
res.removeHeader('Transfer-Encoding');
|
|
1842
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1843
|
-
res.setHeader('Content-Length', Buffer.byteLength(body));
|
|
1844
|
-
originalEnd(body, 'utf8', callback);
|
|
1845
|
-
});
|
|
1846
|
-
return res;
|
|
1847
|
-
};
|
|
1848
|
-
next();
|
|
1849
|
-
}).catch(() => {
|
|
1850
|
-
res.status(503).json({ ok: false, error: 'security-audit-unavailable' });
|
|
1851
|
-
});
|
|
1852
|
-
};
|
|
1853
|
-
parseHubJsonRequest(req, res, next, beginMutationAudit);
|
|
1854
|
-
});
|
|
1449
|
+
app.use(express.json({ limit: '32mb' }));
|
|
1855
1450
|
app.use((req, res, next) => {
|
|
1856
1451
|
if (runtimeRole === 'client' && req.path.startsWith('/api/remote')) {
|
|
1857
1452
|
res.status(403).json(roleGuardResponse(runtimeRole, 'hub'));
|
|
@@ -2006,7 +1601,6 @@ function normalizeTransferFiles(value) {
|
|
|
2006
1601
|
name,
|
|
2007
1602
|
relativePath,
|
|
2008
1603
|
size: byteLength,
|
|
2009
|
-
sha256: crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex'),
|
|
2010
1604
|
mimeType: String(entry?.type || entry?.mimeType || '').slice(0, 160),
|
|
2011
1605
|
lastModified: Number(entry?.lastModified || 0) || 0,
|
|
2012
1606
|
dataBase64
|
|
@@ -2026,31 +1620,16 @@ function normalizeTransferChunk(body = {}) {
|
|
|
2026
1620
|
const totalBytes = Math.max(0, Math.floor(Number(body.totalBytes) || 0));
|
|
2027
1621
|
const byteLength = dataBase64 ? Buffer.byteLength(dataBase64, 'base64') : 0;
|
|
2028
1622
|
const final = body.final === true;
|
|
2029
|
-
const maxFileSizeBytes = Math.max(1, Number(
|
|
2030
|
-
liveDeskSettingsStore.getCached()?.filesAudio?.maxFileSizeBytes
|
|
2031
|
-
|| 1024 * 1024 * 1024));
|
|
2032
1623
|
|
|
2033
1624
|
if (!name || !relativePath || offset > totalBytes || offset + byteLength > totalBytes) {
|
|
2034
1625
|
return { ok: false, error: 'invalid-file-transfer-chunk' };
|
|
2035
1626
|
}
|
|
2036
|
-
if (totalBytes > maxFileSizeBytes) {
|
|
2037
|
-
return { ok: false, error: 'file-transfer-file-too-large', totalBytes };
|
|
2038
|
-
}
|
|
2039
1627
|
if (byteLength > MAX_FILE_TRANSFER_CHUNK_BYTES) {
|
|
2040
1628
|
return { ok: false, error: 'file-transfer-chunk-too-large', byteLength };
|
|
2041
1629
|
}
|
|
2042
1630
|
if (byteLength === 0 && !(final && totalBytes === 0 && offset === 0)) {
|
|
2043
1631
|
return { ok: false, error: 'empty-file-transfer-chunk' };
|
|
2044
1632
|
}
|
|
2045
|
-
const suppliedSha256 = String(body.sha256 || '').trim().toLowerCase();
|
|
2046
|
-
const sha256 = /^[a-f0-9]{64}$/.test(suppliedSha256)
|
|
2047
|
-
? suppliedSha256
|
|
2048
|
-
: final && offset === 0 && totalBytes === byteLength
|
|
2049
|
-
? crypto.createHash('sha256').update(Buffer.from(dataBase64, 'base64')).digest('hex')
|
|
2050
|
-
: '';
|
|
2051
|
-
if (final && !sha256) {
|
|
2052
|
-
return { ok: false, error: 'file-transfer-sha256-required', byteLength };
|
|
2053
|
-
}
|
|
2054
1633
|
|
|
2055
1634
|
return {
|
|
2056
1635
|
ok: true,
|
|
@@ -2062,7 +1641,6 @@ function normalizeTransferChunk(body = {}) {
|
|
|
2062
1641
|
byteLength,
|
|
2063
1642
|
dataBase64,
|
|
2064
1643
|
final,
|
|
2065
|
-
sha256,
|
|
2066
1644
|
mimeType: String(body.type || body.mimeType || '').slice(0, 160),
|
|
2067
1645
|
lastModified: Number(body.lastModified || 0) || 0
|
|
2068
1646
|
}
|
|
@@ -2768,11 +2346,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2768
2346
|
&& bindingIdentity === frameLaneBindingIdentity(currentExpectedBinding)) {
|
|
2769
2347
|
expectedBinding.readySent = currentExpectedBinding?.readySent === true
|
|
2770
2348
|
|| expectedBinding.readySent === true;
|
|
2771
|
-
expectedBinding.replayedKeyFrameSeq = Math.max(
|
|
2772
|
-
0,
|
|
2773
|
-
Number(currentExpectedBinding?.replayedKeyFrameSeq || 0),
|
|
2774
|
-
Number(expectedBinding.replayedKeyFrameSeq || 0)
|
|
2775
|
-
);
|
|
2776
2349
|
ws.liveDeskExpectedStreamBindingsByDeviceId.set(normalizedDeviceId, expectedBinding);
|
|
2777
2350
|
if (!lane.bindingEpochsByDeviceId.has(normalizedDeviceId)) {
|
|
2778
2351
|
lane.bindingEpochsByDeviceId.set(normalizedDeviceId, 1);
|
|
@@ -2811,50 +2384,6 @@ function replaceExpectedFrameBindingForClient(ws, deviceId, expectedBinding = nu
|
|
|
2811
2384
|
return bindingIdentity;
|
|
2812
2385
|
}
|
|
2813
2386
|
|
|
2814
|
-
function replayExpectedBindingKeyFrame(ws, expectedBinding) {
|
|
2815
|
-
if (!ws || ws.readyState !== ws.OPEN || !expectedBinding) {
|
|
2816
|
-
return false;
|
|
2817
|
-
}
|
|
2818
|
-
const deviceId = String(expectedBinding.deviceId || '').trim();
|
|
2819
|
-
const currentBinding = ws.liveDeskExpectedStreamBindingsByDeviceId instanceof Map
|
|
2820
|
-
? ws.liveDeskExpectedStreamBindingsByDeviceId.get(deviceId)
|
|
2821
|
-
: null;
|
|
2822
|
-
const bindingIdentity = frameLaneBindingIdentity(expectedBinding);
|
|
2823
|
-
if (!deviceId
|
|
2824
|
-
|| !bindingIdentity
|
|
2825
|
-
|| bindingIdentity !== frameLaneBindingIdentity(currentBinding)) {
|
|
2826
|
-
return false;
|
|
2827
|
-
}
|
|
2828
|
-
const cached = remoteHub.getFramePayload(deviceId, 'live', {
|
|
2829
|
-
requireKeyFrame: true,
|
|
2830
|
-
streamId: expectedBinding.streamId,
|
|
2831
|
-
sessionId: expectedBinding.sessionId,
|
|
2832
|
-
commandId: expectedBinding.commandId,
|
|
2833
|
-
captureGeneration: expectedBinding.captureGeneration,
|
|
2834
|
-
streamPurpose: expectedBinding.streamPurpose,
|
|
2835
|
-
monitorIndex: expectedBinding.monitorIndex
|
|
2836
|
-
});
|
|
2837
|
-
const frameSeq = Number(cached?.frame?.frameSeq || 0);
|
|
2838
|
-
if (!cached
|
|
2839
|
-
|| cached.frame?.isKeyFrame !== true
|
|
2840
|
-
|| !Number.isSafeInteger(frameSeq)
|
|
2841
|
-
|| frameSeq <= 0
|
|
2842
|
-
|| Number(currentBinding?.replayedKeyFrameSeq || 0) === frameSeq) {
|
|
2843
|
-
return false;
|
|
2844
|
-
}
|
|
2845
|
-
currentBinding.replayedKeyFrameSeq = frameSeq;
|
|
2846
|
-
broadcastRemoteBinaryFrame({
|
|
2847
|
-
kind: 'live',
|
|
2848
|
-
deviceId,
|
|
2849
|
-
frame: cached.frame,
|
|
2850
|
-
payload: cached.payload,
|
|
2851
|
-
mimeType: cached.mimeType,
|
|
2852
|
-
byteLength: cached.byteLength,
|
|
2853
|
-
replayedForBinding: true
|
|
2854
|
-
});
|
|
2855
|
-
return true;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
2387
|
function dropQueuedFrameAtForLane(lane, dropIndex) {
|
|
2859
2388
|
const droppedBefore = lane.dropped;
|
|
2860
2389
|
const droppedItem = removeFrameLaneQueueItem(lane, dropIndex);
|
|
@@ -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:
|
|
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
|
|
4545
|
-
|
|
4546
|
-
const revoked = disconnected === true || credentialRevoked === true;
|
|
4547
|
-
recordSecurityAudit({
|
|
4548
|
-
action: 'security.device.revoked',
|
|
4549
|
-
phase: 'complete',
|
|
4550
|
-
result: revoked ? 'success' : 'not-found',
|
|
4551
|
-
deviceId: req.params.deviceId,
|
|
4552
|
-
authMethod: 'local-admin-session'
|
|
4553
|
-
});
|
|
4554
|
-
res.json({ ok: revoked, revoked, deviceId: req.params.deviceId });
|
|
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
|
-
|
|
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', (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
deviceIds,
|
|
5835
|
-
sessionId: transferId,
|
|
5836
|
-
requestHash: requestAuditHash({ transferId, remoteDirectory, files: normalized.files.map(file => ({ name: file.name, relativePath: file.relativePath, byteLength: file.size })), totalBytes: normalized.totalBytes }),
|
|
5837
|
-
authMethod: 'local-admin-session',
|
|
5838
|
-
details: { queued, acknowledged, total: deviceIds.length, totalBytes: normalized.totalBytes, files: normalized.files.length }
|
|
5839
|
-
});
|
|
5840
|
-
res.json({
|
|
5841
|
-
ok: success,
|
|
5842
|
-
transferId,
|
|
5843
|
-
queued,
|
|
5844
|
-
acknowledged,
|
|
5845
|
-
total: deviceIds.length,
|
|
5846
|
-
totalBytes: normalized.totalBytes,
|
|
5847
|
-
files: normalized.files.length,
|
|
5848
|
-
results,
|
|
5849
|
-
error
|
|
5850
|
-
});
|
|
5851
|
-
} catch (error) {
|
|
5852
|
-
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5853
|
-
}
|
|
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,
|
|
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
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
sessionId: transferId,
|
|
5899
|
-
requestHash: requestAuditHash({ transferId, remoteDirectory, offset: normalized.chunk.offset, byteLength: normalized.chunk.byteLength, final: normalized.chunk.final }),
|
|
5900
|
-
authMethod: 'local-admin-session',
|
|
5901
|
-
details: { queued, acknowledged, total: deviceIds.length, byteLength: normalized.chunk.byteLength, offset: normalized.chunk.offset, final: normalized.chunk.final }
|
|
5902
|
-
});
|
|
5903
|
-
res.json({
|
|
5904
|
-
ok: success,
|
|
5905
|
-
transferId,
|
|
5906
|
-
queued,
|
|
5907
|
-
acknowledged,
|
|
5908
|
-
total: deviceIds.length,
|
|
5909
|
-
byteLength: normalized.chunk.byteLength,
|
|
5910
|
-
offset: normalized.chunk.offset,
|
|
5911
|
-
final: normalized.chunk.final,
|
|
5912
|
-
results,
|
|
5913
|
-
error
|
|
5914
|
-
});
|
|
5915
|
-
} catch (error) {
|
|
5916
|
-
res.status(503).json({ ok: false, error: error?.code || 'security-audit-unavailable' });
|
|
5917
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
authMethod: 'local-admin-websocket'
|
|
6479
|
-
}).then(() => {
|
|
6480
|
-
ws.liveDeskInputAuditReady = true;
|
|
6481
|
-
sendJson(ws, {
|
|
6482
|
-
type: 'RemoteInputSocketReady',
|
|
6483
|
-
protocol: 'livedesk.remote.input.json.v1',
|
|
6484
|
-
clientId: ws.liveDeskInputClientId,
|
|
6485
|
-
timestamp: new Date().toISOString()
|
|
6486
|
-
});
|
|
6487
|
-
}).catch(() => {
|
|
6488
|
-
ws.close(1011, 'security-audit-unavailable');
|
|
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();
|