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