@livedesk/hub 0.1.39 → 0.1.41
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 +1 -1
- package/src/http/hub-ui-session.js +119 -0
- package/src/remote-hub.js +34 -5
- package/src/server.js +151 -47
package/package.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const HUB_UI_SESSION_COOKIE = 'livedesk_hub_ui';
|
|
4
|
+
export const HUB_UI_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
const SAFE_HTTP_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
7
|
+
|
|
8
|
+
function secureEqual(left, right) {
|
|
9
|
+
const leftBuffer = Buffer.from(String(left || ''), 'utf8');
|
|
10
|
+
const rightBuffer = Buffer.from(String(right || ''), 'utf8');
|
|
11
|
+
return leftBuffer.length === rightBuffer.length
|
|
12
|
+
&& leftBuffer.length > 0
|
|
13
|
+
&& crypto.timingSafeEqual(leftBuffer, rightBuffer);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseCookies(value) {
|
|
17
|
+
const result = new Map();
|
|
18
|
+
for (const part of String(value || '').split(';')) {
|
|
19
|
+
const separator = part.indexOf('=');
|
|
20
|
+
if (separator <= 0) continue;
|
|
21
|
+
const key = part.slice(0, separator).trim();
|
|
22
|
+
const rawValue = part.slice(separator + 1).trim();
|
|
23
|
+
if (!key) continue;
|
|
24
|
+
try {
|
|
25
|
+
result.set(key, decodeURIComponent(rawValue));
|
|
26
|
+
} catch {
|
|
27
|
+
result.set(key, rawValue);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function randomToken() {
|
|
34
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHubUiSessionAuthority({
|
|
38
|
+
ttlMs = HUB_UI_SESSION_TTL_MS,
|
|
39
|
+
maxSessions = 32,
|
|
40
|
+
now = () => Date.now()
|
|
41
|
+
} = {}) {
|
|
42
|
+
const sessions = new Map();
|
|
43
|
+
|
|
44
|
+
function purgeExpired() {
|
|
45
|
+
const current = now();
|
|
46
|
+
for (const [sessionId, session] of sessions) {
|
|
47
|
+
if (session.expiresAt <= current) sessions.delete(sessionId);
|
|
48
|
+
}
|
|
49
|
+
while (sessions.size >= maxSessions) {
|
|
50
|
+
sessions.delete(sessions.keys().next().value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function issue(userId) {
|
|
55
|
+
const normalizedUserId = String(userId || '').trim();
|
|
56
|
+
if (!normalizedUserId) throw new Error('hub-ui-session-user-required');
|
|
57
|
+
purgeExpired();
|
|
58
|
+
const sessionId = randomToken();
|
|
59
|
+
const csrfToken = randomToken();
|
|
60
|
+
const expiresAt = now() + ttlMs;
|
|
61
|
+
sessions.set(sessionId, { userId: normalizedUserId, csrfToken, expiresAt });
|
|
62
|
+
return { sessionId, csrfToken, expiresAt };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function authorize(req, currentUserId, {
|
|
66
|
+
requireCsrf = !SAFE_HTTP_METHODS.has(String(req.method || 'GET').toUpperCase())
|
|
67
|
+
} = {}) {
|
|
68
|
+
purgeExpired();
|
|
69
|
+
const sessionId = parseCookies(req.headers?.cookie).get(HUB_UI_SESSION_COOKIE) || '';
|
|
70
|
+
if (!sessionId) {
|
|
71
|
+
return { ok: false, status: 401, error: 'hub-ui-session-required' };
|
|
72
|
+
}
|
|
73
|
+
const session = sessions.get(sessionId);
|
|
74
|
+
if (!session) {
|
|
75
|
+
return { ok: false, status: 401, error: 'hub-ui-session-expired' };
|
|
76
|
+
}
|
|
77
|
+
const normalizedUserId = String(currentUserId || '').trim();
|
|
78
|
+
if (!normalizedUserId || session.userId !== normalizedUserId) {
|
|
79
|
+
sessions.delete(sessionId);
|
|
80
|
+
return { ok: false, status: 403, error: 'hub-ui-session-account-mismatch' };
|
|
81
|
+
}
|
|
82
|
+
if (requireCsrf && !secureEqual(req.headers?.['x-livedesk-csrf'], session.csrfToken)) {
|
|
83
|
+
return { ok: false, status: 403, error: 'hub-ui-csrf-invalid' };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, sessionId, userId: session.userId, expiresAt: session.expiresAt };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
issue,
|
|
90
|
+
authorize,
|
|
91
|
+
revokeAll() {
|
|
92
|
+
sessions.clear();
|
|
93
|
+
},
|
|
94
|
+
sessionCount: () => sessions.size
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function serializeHubUiSessionCookie(sessionId, expiresAt, { secure = false } = {}) {
|
|
99
|
+
const maxAgeSeconds = Math.max(1, Math.floor((Number(expiresAt || 0) - Date.now()) / 1000));
|
|
100
|
+
return [
|
|
101
|
+
`${HUB_UI_SESSION_COOKIE}=${encodeURIComponent(String(sessionId || ''))}`,
|
|
102
|
+
'Path=/',
|
|
103
|
+
'HttpOnly',
|
|
104
|
+
'SameSite=Strict',
|
|
105
|
+
`Max-Age=${maxAgeSeconds}`,
|
|
106
|
+
secure ? 'Secure' : ''
|
|
107
|
+
].filter(Boolean).join('; ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function clearHubUiSessionCookie({ secure = false } = {}) {
|
|
111
|
+
return [
|
|
112
|
+
`${HUB_UI_SESSION_COOKIE}=`,
|
|
113
|
+
'Path=/',
|
|
114
|
+
'HttpOnly',
|
|
115
|
+
'SameSite=Strict',
|
|
116
|
+
'Max-Age=0',
|
|
117
|
+
secure ? 'Secure' : ''
|
|
118
|
+
].filter(Boolean).join('; ');
|
|
119
|
+
}
|
package/src/remote-hub.js
CHANGED
|
@@ -9705,10 +9705,39 @@ export function createRemoteHub(options = {}) {
|
|
|
9705
9705
|
liveStreamReplacementStillPending(activeLiveStream)
|
|
9706
9706
|
? 'stream-restart-superseded'
|
|
9707
9707
|
: 'stream-start-timeout');
|
|
9708
|
-
}
|
|
9709
|
-
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
9710
|
-
|
|
9711
|
-
|
|
9708
|
+
}
|
|
9709
|
+
const commandId = safeString(options.commandId, 128) || crypto.randomUUID();
|
|
9710
|
+
const restartToken = safeString(options.restartToken, 128);
|
|
9711
|
+
if (activeLiveStream
|
|
9712
|
+
&& options.forceRestart === true
|
|
9713
|
+
&& restartToken
|
|
9714
|
+
&& safeString(activeLiveStream.restartToken, 128) === restartToken
|
|
9715
|
+
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
9716
|
+
&& liveStreamIsReusable(activeLiveStream)) {
|
|
9717
|
+
emitRemoteEvent('RemoteLiveStreamRestartTokenReused', device, {
|
|
9718
|
+
streamId,
|
|
9719
|
+
commandId: activeLiveStream.commandId,
|
|
9720
|
+
restartToken,
|
|
9721
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0)
|
|
9722
|
+
});
|
|
9723
|
+
return {
|
|
9724
|
+
ok: true,
|
|
9725
|
+
commandId: activeLiveStream.commandId,
|
|
9726
|
+
sessionId: device.sessionId,
|
|
9727
|
+
streamId,
|
|
9728
|
+
streamPurpose,
|
|
9729
|
+
fps: Number(activeLiveStream.fps || fps),
|
|
9730
|
+
mode: activeLiveStream.mode || transfer.mode,
|
|
9731
|
+
frameMode: activeLiveStream.frameMode || transfer.frameMode,
|
|
9732
|
+
monitorIndex: Number(activeLiveStream.monitorIndex ?? monitorIndex),
|
|
9733
|
+
captureGeneration: Number(activeLiveStream.captureGeneration || 0),
|
|
9734
|
+
ready: liveStreamHasCurrentFrame(activeLiveStream),
|
|
9735
|
+
pending: activeLiveStream.open !== true,
|
|
9736
|
+
reused: true
|
|
9737
|
+
};
|
|
9738
|
+
}
|
|
9739
|
+
if (activeLiveStream
|
|
9740
|
+
&& options.forceRestart !== true
|
|
9712
9741
|
&& options.reuseExisting === true
|
|
9713
9742
|
&& liveStreamMatchesOptions(activeLiveStream, normalized)
|
|
9714
9743
|
&& liveStreamIsReusable(activeLiveStream)) {
|
|
@@ -9838,7 +9867,7 @@ export function createRemoteHub(options = {}) {
|
|
|
9838
9867
|
monitorIndex,
|
|
9839
9868
|
monitorCount: 1,
|
|
9840
9869
|
startedAt: now,
|
|
9841
|
-
restartToken
|
|
9870
|
+
restartToken,
|
|
9842
9871
|
stoppedAt: '',
|
|
9843
9872
|
stopReason: '',
|
|
9844
9873
|
stopPending: false,
|
package/src/server.js
CHANGED
|
@@ -51,8 +51,13 @@ import { normalizeRuntimeAuthSession, runtimeRoleError } from '../../runtime-cor
|
|
|
51
51
|
import { fetchAuthResponse, AUTH_REQUEST_TIMEOUT_MS } from '../../runtime-core/src/auth-http.js';
|
|
52
52
|
import { PRODUCTION_SUPABASE_PUBLISHABLE_KEY, PRODUCTION_SUPABASE_URL } from '../../runtime-core/src/auth-config.js';
|
|
53
53
|
import { createHubRuntime } from './runtime/hub-runtime.js';
|
|
54
|
-
import { hubRuntimeStatus } from './runtime/hub-runtime-status.js';
|
|
55
|
-
import { roleGuardResponse } from './http/hub-role-guard.js';
|
|
54
|
+
import { hubRuntimeStatus } from './runtime/hub-runtime-status.js';
|
|
55
|
+
import { roleGuardResponse } from './http/hub-role-guard.js';
|
|
56
|
+
import {
|
|
57
|
+
clearHubUiSessionCookie,
|
|
58
|
+
createHubUiSessionAuthority,
|
|
59
|
+
serializeHubUiSessionCookie
|
|
60
|
+
} from './http/hub-ui-session.js';
|
|
56
61
|
|
|
57
62
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
58
63
|
const webDistCandidates = [
|
|
@@ -182,9 +187,10 @@ const persistentSessionGcToken = persistentSessionGcEnabled
|
|
|
182
187
|
? String(process.env.LIVEDESK_PERSISTENT_SESSION_TEST_TOKEN || '').trim()
|
|
183
188
|
: '';
|
|
184
189
|
let connectedDeviceCount = 0;
|
|
185
|
-
let runtimeAccessToken = '';
|
|
186
|
-
let runtimeRefreshToken = '';
|
|
187
|
-
let runtimeAccessTokenExpiresAt = 0;
|
|
190
|
+
let runtimeAccessToken = '';
|
|
191
|
+
let runtimeRefreshToken = '';
|
|
192
|
+
let runtimeAccessTokenExpiresAt = 0;
|
|
193
|
+
const hubUiSessionAuthority = createHubUiSessionAuthority();
|
|
188
194
|
let roleWatchInFlight = false;
|
|
189
195
|
let verifiedLicense = {
|
|
190
196
|
userId: '',
|
|
@@ -485,11 +491,12 @@ function clearPersistedRuntimeSession() {
|
|
|
485
491
|
return writePrivateRuntimeAuthState(state);
|
|
486
492
|
}
|
|
487
493
|
|
|
488
|
-
function clearRuntimeSession() {
|
|
489
|
-
runtimeAccessToken = '';
|
|
490
|
-
runtimeRefreshToken = '';
|
|
491
|
-
runtimeAccessTokenExpiresAt = 0;
|
|
492
|
-
|
|
494
|
+
function clearRuntimeSession() {
|
|
495
|
+
runtimeAccessToken = '';
|
|
496
|
+
runtimeRefreshToken = '';
|
|
497
|
+
runtimeAccessTokenExpiresAt = 0;
|
|
498
|
+
hubUiSessionAuthority.revokeAll();
|
|
499
|
+
runtimeManager.setAuthenticated(false);
|
|
493
500
|
try { clearPersistedRuntimeSession(); } catch { /* runtime auth is already invalid */ }
|
|
494
501
|
}
|
|
495
502
|
|
|
@@ -1405,7 +1412,7 @@ function requestHostname(req) {
|
|
|
1405
1412
|
}
|
|
1406
1413
|
}
|
|
1407
1414
|
|
|
1408
|
-
function isTrustedBrowserRequest(req) {
|
|
1415
|
+
function isTrustedBrowserRequest(req) {
|
|
1409
1416
|
const origin = String(req.headers.origin || '').trim();
|
|
1410
1417
|
if (!origin) {
|
|
1411
1418
|
const fetchSite = String(req.headers['sec-fetch-site'] || '').toLowerCase();
|
|
@@ -1422,22 +1429,59 @@ function isTrustedBrowserRequest(req) {
|
|
|
1422
1429
|
const targetHost = requestHostname(req);
|
|
1423
1430
|
return (originHost === targetHost && isPrivateHubHostname(targetHost))
|
|
1424
1431
|
|| (isLoopbackHostname(originHost) && isLoopbackHostname(targetHost));
|
|
1425
|
-
} catch {
|
|
1426
|
-
return false;
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1432
|
+
} catch {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function isSecureHubHttpRequest(req) {
|
|
1438
|
+
return req.socket?.encrypted === true
|
|
1439
|
+
|| String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https';
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function currentBoundHubUserId() {
|
|
1443
|
+
return String(
|
|
1444
|
+
runtimeManager.getSnapshot().userId
|
|
1445
|
+
|| readPersistedRuntimeSession()?.user?.id
|
|
1446
|
+
|| ''
|
|
1447
|
+
).trim();
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function isPublicHubApiRequest(req) {
|
|
1451
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
1452
|
+
const path = String(req.path || '');
|
|
1453
|
+
return (method === 'GET' && (path === '/api/health' || path === '/api/runtime/status' || path === '/api/auth/status'))
|
|
1454
|
+
|| (method === 'POST' && path === '/api/auth/session');
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function isTrustedNativeLoopbackRequest(req) {
|
|
1458
|
+
return isLoopbackAddress(req.socket?.remoteAddress)
|
|
1459
|
+
&& !String(req.headers.origin || '').trim()
|
|
1460
|
+
&& !String(req.headers['sec-fetch-site'] || '').trim();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function authorizeHubUiRequest(req) {
|
|
1464
|
+
if (process.env.LIVEDESK_TEST_MODE === '1') {
|
|
1465
|
+
return { ok: true, explicitTestMode: true };
|
|
1466
|
+
}
|
|
1467
|
+
if (isTrustedNativeLoopbackRequest(req)) {
|
|
1468
|
+
return { ok: true, nativeLoopback: true };
|
|
1469
|
+
}
|
|
1470
|
+
return hubUiSessionAuthority.authorize(req, currentBoundHubUserId());
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
app.use((req, res, next) => {
|
|
1431
1474
|
if (!isTrustedBrowserRequest(req)) {
|
|
1432
1475
|
res.status(403).json({ ok: false, error: 'untrusted-hub-origin' });
|
|
1433
1476
|
return;
|
|
1434
1477
|
}
|
|
1435
1478
|
const origin = String(req.headers.origin || '').trim();
|
|
1436
1479
|
if (origin) {
|
|
1437
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1438
|
-
res.setHeader('Access-Control-Allow-
|
|
1480
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
1481
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
1482
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
1439
1483
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
1440
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1484
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-LiveDesk-CSRF');
|
|
1441
1485
|
res.setHeader('Vary', 'Origin, Access-Control-Request-Headers, Access-Control-Request-Private-Network');
|
|
1442
1486
|
}
|
|
1443
1487
|
if (req.method === 'OPTIONS') {
|
|
@@ -1446,8 +1490,21 @@ app.use((req, res, next) => {
|
|
|
1446
1490
|
}
|
|
1447
1491
|
next();
|
|
1448
1492
|
});
|
|
1449
|
-
app.use(express.json({ limit: '32mb' }));
|
|
1450
|
-
app.use((req, res, next) => {
|
|
1493
|
+
app.use(express.json({ limit: '32mb' }));
|
|
1494
|
+
app.use((req, res, next) => {
|
|
1495
|
+
if (!req.path.startsWith('/api/') || isPublicHubApiRequest(req)) {
|
|
1496
|
+
next();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const authorization = authorizeHubUiRequest(req);
|
|
1500
|
+
if (!authorization.ok) {
|
|
1501
|
+
noStore(res);
|
|
1502
|
+
res.status(authorization.status).json({ ok: false, error: authorization.error });
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
next();
|
|
1506
|
+
});
|
|
1507
|
+
app.use((req, res, next) => {
|
|
1451
1508
|
if (runtimeRole === 'client' && req.path.startsWith('/api/remote')) {
|
|
1452
1509
|
res.status(403).json(roleGuardResponse(runtimeRole, 'hub'));
|
|
1453
1510
|
return;
|
|
@@ -4351,10 +4408,19 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
4351
4408
|
res.status(400).json({ ok: false, error: normalized.error });
|
|
4352
4409
|
return;
|
|
4353
4410
|
}
|
|
4354
|
-
const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
|
|
4355
|
-
try {
|
|
4356
|
-
const user = await verifySupabaseUser(accessToken);
|
|
4357
|
-
|
|
4411
|
+
const { access_token: accessToken, refresh_token: refreshToken, expires_at: expiresAt } = normalized.session;
|
|
4412
|
+
try {
|
|
4413
|
+
const user = await verifySupabaseUser(accessToken);
|
|
4414
|
+
const boundUserId = currentBoundHubUserId();
|
|
4415
|
+
if (boundUserId && boundUserId !== user.id) {
|
|
4416
|
+
res.status(403).json({ ok: false, error: 'hub-account-mismatch' });
|
|
4417
|
+
return;
|
|
4418
|
+
}
|
|
4419
|
+
if (!boundUserId && !isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
4420
|
+
res.status(403).json({ ok: false, error: 'hub-first-account-binding-must-be-local' });
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
runtimeAccessToken = accessToken;
|
|
4358
4424
|
runtimeRefreshToken = refreshToken;
|
|
4359
4425
|
runtimeAccessTokenExpiresAt = normalizeSessionExpiryMs(expiresAt);
|
|
4360
4426
|
runtimeManager.setAuthenticated(true, user.id);
|
|
@@ -4364,13 +4430,32 @@ app.post('/api/auth/session', async (req, res) => {
|
|
|
4364
4430
|
expires_at: expiresAt,
|
|
4365
4431
|
user
|
|
4366
4432
|
});
|
|
4367
|
-
const hostTarget = runtimeRole === 'hub'
|
|
4368
|
-
?
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4433
|
+
const hostTarget = runtimeRole === 'hub'
|
|
4434
|
+
? {
|
|
4435
|
+
ok: true,
|
|
4436
|
+
pending: true,
|
|
4437
|
+
active: remoteHub.getStatus({ includeSecrets: false }).hostTargetActive === true
|
|
4438
|
+
}
|
|
4439
|
+
: { ok: true, pending: false, active: false };
|
|
4440
|
+
const uiSession = hubUiSessionAuthority.issue(user.id);
|
|
4441
|
+
res.setHeader('Set-Cookie', serializeHubUiSessionCookie(uiSession.sessionId, uiSession.expiresAt, {
|
|
4442
|
+
secure: isSecureHubHttpRequest(req)
|
|
4443
|
+
}));
|
|
4444
|
+
res.json({
|
|
4445
|
+
ok: true,
|
|
4446
|
+
authenticated: true,
|
|
4447
|
+
persisted,
|
|
4448
|
+
userId: user.id,
|
|
4449
|
+
role: runtimeRole,
|
|
4450
|
+
hostTarget,
|
|
4451
|
+
uiSession: {
|
|
4452
|
+
csrfToken: uiSession.csrfToken,
|
|
4453
|
+
expiresAt: uiSession.expiresAt
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
if (runtimeRole === 'hub') {
|
|
4457
|
+
scheduleAuthenticatedHubHostTargetPublication('session-received');
|
|
4458
|
+
}
|
|
4374
4459
|
} catch (error) {
|
|
4375
4460
|
const message = error instanceof Error ? error.message : String(error);
|
|
4376
4461
|
const status = authVerificationHttpStatus(message);
|
|
@@ -4656,24 +4741,36 @@ async function clearHubHostTarget(reason = 'shutdown') {
|
|
|
4656
4741
|
return { ok: !error, error, lease: getHubHostTargetLeaseStatus() };
|
|
4657
4742
|
}
|
|
4658
4743
|
|
|
4659
|
-
function startHubHostTargetLeaseRenewal() {
|
|
4744
|
+
function startHubHostTargetLeaseRenewal() {
|
|
4660
4745
|
if (hubHostTargetRenewTimer || runtimeRole !== 'hub') {
|
|
4661
4746
|
return;
|
|
4662
4747
|
}
|
|
4663
4748
|
hubHostTargetRenewTimer = setInterval(() => {
|
|
4664
4749
|
void publishHubHostTargetWithPendingRoleTakeover('renewal');
|
|
4665
4750
|
}, HUB_HOST_TARGET_RENEW_MS);
|
|
4666
|
-
hubHostTargetRenewTimer.unref?.();
|
|
4667
|
-
}
|
|
4751
|
+
hubHostTargetRenewTimer.unref?.();
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
function scheduleAuthenticatedHubHostTargetPublication(reason = 'session-received') {
|
|
4755
|
+
startHubHostTargetLeaseRenewal();
|
|
4756
|
+
setImmediate(() => {
|
|
4757
|
+
void publishHubHostTargetWithPendingRoleTakeover(reason).catch(error => {
|
|
4758
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4759
|
+
updateHubHostTargetLeaseState({ state: 'error', lastError: message });
|
|
4760
|
+
console.error(`[LiveDesk Hub] Host target ${reason} background publish failed: ${message}`);
|
|
4761
|
+
});
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4668
4764
|
|
|
4669
|
-
app.delete('/api/auth/session', async (
|
|
4765
|
+
app.delete('/api/auth/session', async (req, res) => {
|
|
4670
4766
|
noStore(res);
|
|
4671
4767
|
const hostTarget = runtimeRole === 'hub'
|
|
4672
4768
|
? await clearHubHostTarget('logout')
|
|
4673
4769
|
: { ok: true, active: false };
|
|
4674
|
-
clearRuntimeSession();
|
|
4675
|
-
res.
|
|
4676
|
-
});
|
|
4770
|
+
clearRuntimeSession();
|
|
4771
|
+
res.setHeader('Set-Cookie', clearHubUiSessionCookie({ secure: isSecureHubHttpRequest(req) }));
|
|
4772
|
+
res.json({ ok: true, authenticated: false, role: runtimeRole, hostTarget });
|
|
4773
|
+
});
|
|
4677
4774
|
|
|
4678
4775
|
app.get('/api/hub/status', (_req, res) => {
|
|
4679
4776
|
noStore(res);
|
|
@@ -5474,13 +5571,20 @@ if (existsSync(webIndexPath)) {
|
|
|
5474
5571
|
});
|
|
5475
5572
|
}
|
|
5476
5573
|
|
|
5477
|
-
httpServer.on('upgrade', (req, socket, head) => {
|
|
5478
|
-
if (!isTrustedBrowserRequest(req)) {
|
|
5479
|
-
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
5480
|
-
socket.destroy();
|
|
5481
|
-
return;
|
|
5482
|
-
}
|
|
5483
|
-
|
|
5574
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
5575
|
+
if (!isTrustedBrowserRequest(req)) {
|
|
5576
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
5577
|
+
socket.destroy();
|
|
5578
|
+
return;
|
|
5579
|
+
}
|
|
5580
|
+
const authorization = authorizeHubUiRequest(req);
|
|
5581
|
+
if (!authorization.ok) {
|
|
5582
|
+
const label = authorization.status === 403 ? 'Forbidden' : 'Unauthorized';
|
|
5583
|
+
socket.write(`HTTP/1.1 ${authorization.status} ${label}\r\nConnection: close\r\n\r\n`);
|
|
5584
|
+
socket.destroy();
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5587
|
+
try {
|
|
5484
5588
|
const parsed = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
5485
5589
|
if (parsed.pathname === '/api/remote/frames/ws') {
|
|
5486
5590
|
frameWss.handleUpgrade(req, socket, head, ws => frameWss.emit('connection', ws, req));
|