@livedesk/hub 0.1.57 → 0.1.59
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 +3 -3
- package/src/auth/workspace-access.js +176 -0
- package/src/console-direct.js +469 -11
- package/src/console-direct.test.mjs +446 -5
- package/src/http/hub-ui-session.js +155 -32
- package/src/server.js +1141 -328
package/src/console-direct.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createDirectConsoleWireBudget,
|
|
9
9
|
encodeDirectConsoleWireMessage
|
|
10
10
|
} from '../../runtime-core/src/console-direct-wire.js';
|
|
11
|
+
import { workspaceRoleCanControl, workspaceRoleCanRequest } from './auth/workspace-access.js';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_SIGNAL_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
|
|
13
14
|
const DEFAULT_STUN_URLS = Object.freeze(['stun:stun.cloudflare.com:3478']);
|
|
@@ -34,10 +35,15 @@ const ICE_CONNECT_TIMEOUT_MS = 10_000;
|
|
|
34
35
|
const PEER_DISCONNECTED_TIMEOUT_MS = 5_000;
|
|
35
36
|
const ACCESS_TOKEN_TIMEOUT_MS = 8_000;
|
|
36
37
|
const SIGNAL_READY_TIMEOUT_MS = 10_000;
|
|
38
|
+
const SIGNAL_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
39
|
+
const SIGNAL_HEARTBEAT_TIMEOUT_MS = 10_000;
|
|
37
40
|
const CONTROL_ASSEMBLY_TIMEOUT_MS = 15_000;
|
|
38
41
|
const MEDIA_ASSEMBLY_TIMEOUT_MS = 500;
|
|
39
42
|
const LOGICAL_BIND_TIMEOUT_MS = 5_000;
|
|
40
43
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
44
|
+
const CONSOLE_ACCESS_MAX_FUTURE_MS = 10 * 60 * 1000;
|
|
45
|
+
const REVOKED_MEMBER_FENCE_MS = 60_000;
|
|
46
|
+
const MAX_REVOKED_MEMBER_FENCES = 128;
|
|
41
47
|
|
|
42
48
|
function normalizeSignalUrl(value) {
|
|
43
49
|
const source = String(value || DEFAULT_SIGNAL_URL).trim();
|
|
@@ -55,6 +61,16 @@ function normalizeSignalUrl(value) {
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
function signalHttpEndpoint(signalUrl, pathname) {
|
|
65
|
+
if (!(signalUrl instanceof URL)) return null;
|
|
66
|
+
const url = new URL(signalUrl.href);
|
|
67
|
+
url.protocol = url.protocol === 'ws:' ? 'http:' : 'https:';
|
|
68
|
+
url.pathname = pathname;
|
|
69
|
+
url.search = '';
|
|
70
|
+
url.hash = '';
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
function normalizeStunUrls(value) {
|
|
59
75
|
const values = Array.isArray(value)
|
|
60
76
|
? value
|
|
@@ -150,6 +166,7 @@ function allowedHttpRequest(method, pathname) {
|
|
|
150
166
|
|| pathname === '/api/runtime/status'
|
|
151
167
|
|| pathname === '/api/runtime/restart'
|
|
152
168
|
|| pathname === '/api/auth/status'
|
|
169
|
+
|| pathname === '/api/auth/session'
|
|
153
170
|
|| pathname === '/api/remote/status'
|
|
154
171
|
|| pathname === '/api/hub/status'
|
|
155
172
|
|| pathname === '/api/update/status'
|
|
@@ -160,10 +177,12 @@ function allowedHttpRequest(method, pathname) {
|
|
|
160
177
|
|| /^\/api\/captures(?:\/|$)/.test(pathname)
|
|
161
178
|
|| /^\/api\/capture-sessions(?:\/|$)/.test(pathname)
|
|
162
179
|
|| /^\/api\/remote\/devices(?:\/|$)/.test(pathname)
|
|
180
|
+
|| pathname === '/api/remote/wall-preferences'
|
|
163
181
|
|| /^\/api\/remote\/frames$/.test(pathname)
|
|
164
182
|
|| /^\/api\/remote\/filesystem(?:\/|$)/.test(pathname)
|
|
165
183
|
|| /^\/api\/remote\/files(?:\/|$)/.test(pathname)
|
|
166
184
|
|| /^\/api\/remote\/tasks(?:\/|$)/.test(pathname)
|
|
185
|
+
|| /^\/api\/remote\/workspace(?:\/|$)/.test(pathname)
|
|
167
186
|
|| /^\/api\/remote\/license(?:\/sync)?$/.test(pathname)) {
|
|
168
187
|
return pathname !== '/api/remote/registry-credentials'
|
|
169
188
|
&& pathname !== '/api/remote/pairing-pin'
|
|
@@ -269,6 +288,7 @@ async function readBoundedResponseBytes(response, maxBytes) {
|
|
|
269
288
|
|
|
270
289
|
export function createHubConsoleDirect(options = {}) {
|
|
271
290
|
const signalUrl = normalizeSignalUrl(options.url);
|
|
291
|
+
const workspaceReauthUrl = signalHttpEndpoint(signalUrl, '/v2/rtc/reauth');
|
|
272
292
|
const deviceId = String(options.deviceId || '').trim();
|
|
273
293
|
const requestedHubInstanceId = String(options.hubInstanceId || '').trim();
|
|
274
294
|
const hubInstanceId = UUID_PATTERN.test(requestedHubInstanceId)
|
|
@@ -283,6 +303,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
283
303
|
const getAccessToken = typeof options.getAccessToken === 'function'
|
|
284
304
|
? options.getAccessToken
|
|
285
305
|
: async () => String(options.accessToken || '').trim();
|
|
306
|
+
const getWorkspaceAccess = typeof options.getWorkspaceAccess === 'function'
|
|
307
|
+
? options.getWorkspaceAccess
|
|
308
|
+
: () => options.workspaceAccess || null;
|
|
309
|
+
const consoleProxyToken = String(options.consoleProxyToken || '').trim();
|
|
286
310
|
const createPeerConnection = typeof options.createPeerConnection === 'function'
|
|
287
311
|
? options.createPeerConnection
|
|
288
312
|
: (name, config) => new nodeDataChannel.PeerConnection(name, config);
|
|
@@ -302,6 +326,20 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
302
326
|
1,
|
|
303
327
|
Number(options.signalReadyTimeoutMs) || SIGNAL_READY_TIMEOUT_MS
|
|
304
328
|
);
|
|
329
|
+
const signalHeartbeatIntervalMs = Math.max(
|
|
330
|
+
1,
|
|
331
|
+
Number(options.signalHeartbeatIntervalMs) || SIGNAL_HEARTBEAT_INTERVAL_MS
|
|
332
|
+
);
|
|
333
|
+
const signalHeartbeatTimeoutMs = Math.max(
|
|
334
|
+
1,
|
|
335
|
+
Number(options.signalHeartbeatTimeoutMs) || SIGNAL_HEARTBEAT_TIMEOUT_MS
|
|
336
|
+
);
|
|
337
|
+
const scheduleSignalHeartbeatTimer = typeof options.scheduleSignalHeartbeatTimer === 'function'
|
|
338
|
+
? options.scheduleSignalHeartbeatTimer
|
|
339
|
+
: setTimeout;
|
|
340
|
+
const cancelSignalHeartbeatTimer = typeof options.cancelSignalHeartbeatTimer === 'function'
|
|
341
|
+
? options.cancelSignalHeartbeatTimer
|
|
342
|
+
: clearTimeout;
|
|
305
343
|
const mediaAssemblyTimeoutMs = Math.max(
|
|
306
344
|
1,
|
|
307
345
|
Number(options.mediaAssemblyTimeoutMs) || MEDIA_ASSEMBLY_TIMEOUT_MS
|
|
@@ -311,12 +349,15 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
311
349
|
Number(options.logicalBindTimeoutMs) || LOGICAL_BIND_TIMEOUT_MS
|
|
312
350
|
);
|
|
313
351
|
const peers = new Map();
|
|
352
|
+
const revokedMembers = new Map();
|
|
314
353
|
let signalSocket = null;
|
|
315
354
|
let signalGeneration = 0;
|
|
316
355
|
let peerGeneration = 0;
|
|
317
356
|
let retryTimer = null;
|
|
318
357
|
let accessTokenAttempt = null;
|
|
358
|
+
let workspaceReauthAttempt = null;
|
|
319
359
|
let signalReadyDeadline = null;
|
|
360
|
+
let signalHeartbeat = null;
|
|
320
361
|
let retryAttempt = 0;
|
|
321
362
|
let retryNotBeforeAt = 0;
|
|
322
363
|
let nextRetryAt = '';
|
|
@@ -331,10 +372,91 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
331
372
|
let malformedWireCloses = 0;
|
|
332
373
|
let wireBudgetCleanupFailures = 0;
|
|
333
374
|
|
|
375
|
+
const currentHubWorkspaceAccess = () => {
|
|
376
|
+
const access = getWorkspaceAccess() || {};
|
|
377
|
+
return {
|
|
378
|
+
workspaceId: String(access.workspaceId || '').trim(),
|
|
379
|
+
workspaceKind: String(access.workspaceKind || '').trim().toLowerCase(),
|
|
380
|
+
role: String(access.role || '').trim().toLowerCase(),
|
|
381
|
+
membershipRevision: Math.max(0, Number(access.membershipRevision || 0))
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const consoleWorkspaceAccess = payload => {
|
|
386
|
+
const workspaceId = String(payload?.workspaceId || '').trim();
|
|
387
|
+
const workspaceKind = String(payload?.workspaceKind || '').trim().toLowerCase();
|
|
388
|
+
const userId = String(payload?.memberUserId || '').trim();
|
|
389
|
+
const role = String(payload?.memberRole || '').trim().toLowerCase();
|
|
390
|
+
const membershipRevision = Number(payload?.membershipRevision || 0);
|
|
391
|
+
const accessExpiresAt = Number(payload?.accessExpiresAt || 0);
|
|
392
|
+
const hubAccess = currentHubWorkspaceAccess();
|
|
393
|
+
if (!hubAccess.workspaceId && !workspaceId && !workspaceKind && !userId && !role && accessExpiresAt === 0) {
|
|
394
|
+
return Object.freeze({
|
|
395
|
+
workspaceId: 'unconfigured-personal-workspace',
|
|
396
|
+
workspaceKind: 'personal',
|
|
397
|
+
userId: 'unconfigured-personal-user',
|
|
398
|
+
role: 'owner',
|
|
399
|
+
membershipRevision: 0,
|
|
400
|
+
accessExpiresAt: Date.now() + CONSOLE_ACCESS_MAX_FUTURE_MS
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (!workspaceId
|
|
404
|
+
|| !['personal', 'team'].includes(workspaceKind)
|
|
405
|
+
|| !userId
|
|
406
|
+
|| workspaceId !== hubAccess.workspaceId
|
|
407
|
+
|| workspaceKind !== hubAccess.workspaceKind
|
|
408
|
+
|| !workspaceRoleCanControl(role)
|
|
409
|
+
|| !Number.isSafeInteger(membershipRevision)
|
|
410
|
+
|| membershipRevision < 0
|
|
411
|
+
|| !Number.isFinite(accessExpiresAt)
|
|
412
|
+
|| (workspaceKind === 'team' && (accessExpiresAt <= Date.now()
|
|
413
|
+
|| accessExpiresAt > Date.now() + CONSOLE_ACCESS_MAX_FUTURE_MS))
|
|
414
|
+
|| (workspaceKind === 'personal' && accessExpiresAt !== 0)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
return Object.freeze({ workspaceId, workspaceKind, userId, role, membershipRevision, accessExpiresAt });
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const consoleProxyHeaders = owner => ({
|
|
421
|
+
'X-LiveDesk-Console-Proxy': consoleProxyToken,
|
|
422
|
+
'X-LiveDesk-Workspace-Id': owner.workspaceAccess.workspaceId,
|
|
423
|
+
'X-LiveDesk-Workspace-Kind': owner.workspaceAccess.workspaceKind,
|
|
424
|
+
'X-LiveDesk-Workspace-User-Id': owner.workspaceAccess.userId,
|
|
425
|
+
'X-LiveDesk-Workspace-Role': owner.workspaceAccess.role,
|
|
426
|
+
'X-LiveDesk-Membership-Revision': String(owner.workspaceAccess.membershipRevision),
|
|
427
|
+
'X-LiveDesk-Workspace-Access-Expires-At': String(owner.workspaceAccess.accessExpiresAt)
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const ownerWorkspaceAccessCurrent = owner => owner?.workspaceAccess
|
|
431
|
+
&& (owner.workspaceAccess.workspaceId === currentHubWorkspaceAccess().workspaceId
|
|
432
|
+
|| (!currentHubWorkspaceAccess().workspaceId
|
|
433
|
+
&& owner.workspaceAccess.workspaceId === 'unconfigured-personal-workspace'))
|
|
434
|
+
&& workspaceRoleCanControl(owner.workspaceAccess.role)
|
|
435
|
+
&& (owner.workspaceAccess.workspaceKind === 'personal'
|
|
436
|
+
|| owner.workspaceAccess.accessExpiresAt > Date.now());
|
|
437
|
+
|
|
334
438
|
const isOwnerActive = owner => !owner.retired
|
|
335
439
|
&& owner.hubEpoch === hubEpoch
|
|
336
440
|
&& peers.get(owner.consoleId) === owner
|
|
337
|
-
&& owner.generation > 0
|
|
441
|
+
&& owner.generation > 0
|
|
442
|
+
&& ownerWorkspaceAccessCurrent(owner);
|
|
443
|
+
|
|
444
|
+
const memberFenceKey = (workspaceId, userId) => `${String(workspaceId || '').trim()}:${String(userId || '').trim()}`;
|
|
445
|
+
|
|
446
|
+
const pruneRevokedMembers = () => {
|
|
447
|
+
const now = Date.now();
|
|
448
|
+
for (const [key, expiresAt] of revokedMembers) {
|
|
449
|
+
if (expiresAt <= now) revokedMembers.delete(key);
|
|
450
|
+
}
|
|
451
|
+
while (revokedMembers.size > MAX_REVOKED_MEMBER_FENCES) {
|
|
452
|
+
revokedMembers.delete(revokedMembers.keys().next().value);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const memberIsFenced = access => {
|
|
457
|
+
pruneRevokedMembers();
|
|
458
|
+
return Number(revokedMembers.get(memberFenceKey(access?.workspaceId, access?.userId)) || 0) > Date.now();
|
|
459
|
+
};
|
|
338
460
|
|
|
339
461
|
const sendSignal = payload => {
|
|
340
462
|
if (!socketIsOpen(signalSocket, SignalingWebSocketImpl)) return false;
|
|
@@ -418,6 +540,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
418
540
|
owner.iceTimer = null;
|
|
419
541
|
clearTimeout(owner.disconnectTimer);
|
|
420
542
|
owner.disconnectTimer = null;
|
|
543
|
+
clearTimeout(owner.accessTimer);
|
|
544
|
+
owner.accessTimer = null;
|
|
421
545
|
for (const pending of owner.pendingHttp.values()) {
|
|
422
546
|
clearTimeout(pending.timeout);
|
|
423
547
|
pending.cancelled = true;
|
|
@@ -447,6 +571,24 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
447
571
|
}
|
|
448
572
|
};
|
|
449
573
|
|
|
574
|
+
const armOwnerAccessDeadline = owner => {
|
|
575
|
+
clearTimeout(owner.accessTimer);
|
|
576
|
+
owner.accessTimer = null;
|
|
577
|
+
if (owner.workspaceAccess?.workspaceKind !== 'team') return;
|
|
578
|
+
const delay = Number(owner.workspaceAccess.accessExpiresAt || 0) - Date.now();
|
|
579
|
+
if (delay <= 0) {
|
|
580
|
+
retirePeer(owner, 'console-workspace-access-expired');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const generation = owner.generation;
|
|
584
|
+
owner.accessTimer = setTimeout(() => {
|
|
585
|
+
owner.accessTimer = null;
|
|
586
|
+
if (peers.get(owner.consoleId) !== owner || owner.generation !== generation || owner.retired) return;
|
|
587
|
+
retirePeer(owner, 'console-workspace-access-expired');
|
|
588
|
+
}, delay);
|
|
589
|
+
owner.accessTimer.unref?.();
|
|
590
|
+
};
|
|
591
|
+
|
|
450
592
|
const retireAllPeers = reason => {
|
|
451
593
|
for (const owner of [...peers.values()]) retirePeer(owner, reason, { notify: false });
|
|
452
594
|
};
|
|
@@ -599,6 +741,15 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
599
741
|
sendHttpResponse(owner, requestId, { error: 'console-route-not-allowed' });
|
|
600
742
|
return;
|
|
601
743
|
}
|
|
744
|
+
if (!workspaceRoleCanRequest(owner.workspaceAccess?.role, method, requestUrl.pathname)) {
|
|
745
|
+
sendHttpResponse(owner, requestId, { status: 403, error: 'workspace-owner-required' });
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (!ownerWorkspaceAccessCurrent(owner)) {
|
|
749
|
+
sendHttpResponse(owner, requestId, { status: 403, error: 'console-workspace-access-expired' });
|
|
750
|
+
retirePeer(owner, 'console-workspace-access-expired');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
602
753
|
if (owner.pendingHttp.has(requestId)) {
|
|
603
754
|
sendHttpResponse(owner, requestId, { error: 'console-http-request-duplicate' });
|
|
604
755
|
return;
|
|
@@ -607,12 +758,20 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
607
758
|
sendHttpResponse(owner, requestId, { error: 'console-http-capacity-reached' });
|
|
608
759
|
return;
|
|
609
760
|
}
|
|
761
|
+
const isAuthSessionBootstrap = method === 'POST'
|
|
762
|
+
&& requestUrl.pathname === '/api/auth/session';
|
|
610
763
|
let body;
|
|
611
|
-
|
|
612
|
-
body =
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
764
|
+
if (isAuthSessionBootstrap) {
|
|
765
|
+
body = Buffer.from(JSON.stringify({
|
|
766
|
+
workspaceId: owner.workspaceAccess.workspaceId
|
|
767
|
+
}), 'utf8');
|
|
768
|
+
} else {
|
|
769
|
+
try {
|
|
770
|
+
body = decodeBase64Bounded(payload?.bodyBase64, MAX_HTTP_REQUEST_BYTES);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
sendHttpResponse(owner, requestId, { error: error instanceof Error ? error.message : error });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
616
775
|
}
|
|
617
776
|
const controller = new AbortController();
|
|
618
777
|
const pending = { controller, timeout: null, cancelled: false };
|
|
@@ -625,7 +784,12 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
625
784
|
try {
|
|
626
785
|
const response = await fetchImpl(requestUrl.href, {
|
|
627
786
|
method,
|
|
628
|
-
headers:
|
|
787
|
+
headers: {
|
|
788
|
+
...(isAuthSessionBootstrap
|
|
789
|
+
? { 'content-type': 'application/json' }
|
|
790
|
+
: safeContentHeaders(payload?.headers)),
|
|
791
|
+
...consoleProxyHeaders(owner)
|
|
792
|
+
},
|
|
629
793
|
body: ['GET', 'HEAD'].includes(method) || body.byteLength === 0 ? undefined : body,
|
|
630
794
|
signal: controller.signal
|
|
631
795
|
});
|
|
@@ -697,7 +861,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
697
861
|
if (channelState.openRequested
|
|
698
862
|
|| channelId !== channelState.channelId
|
|
699
863
|
|| purpose !== channelState.purpose
|
|
700
|
-
|| !allowedWebSocketPath(path, purpose)
|
|
864
|
+
|| !allowedWebSocketPath(path, purpose)
|
|
865
|
+
|| !workspaceRoleCanRequest(owner.workspaceAccess?.role, 'GET', String(path || '').split('?')[0])) {
|
|
701
866
|
sendLifecycleControl(owner, {
|
|
702
867
|
type: 'ws-error',
|
|
703
868
|
channelId: channelState.channelId,
|
|
@@ -710,6 +875,15 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
710
875
|
});
|
|
711
876
|
return;
|
|
712
877
|
}
|
|
878
|
+
if (!ownerWorkspaceAccessCurrent(owner)) {
|
|
879
|
+
sendLifecycleControl(owner, {
|
|
880
|
+
type: 'ws-error',
|
|
881
|
+
channelId: channelState.channelId,
|
|
882
|
+
error: 'console-workspace-access-expired'
|
|
883
|
+
});
|
|
884
|
+
retirePeer(owner, 'console-workspace-access-expired');
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
713
887
|
channelState.openRequested = true;
|
|
714
888
|
const localUrl = new URL(path, httpBaseUrl);
|
|
715
889
|
const httpOrigin = new URL(httpBaseUrl).origin;
|
|
@@ -720,7 +894,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
720
894
|
localUrl.protocol = localUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
721
895
|
let socket;
|
|
722
896
|
try {
|
|
723
|
-
socket = new LocalWebSocketImpl(localUrl, {
|
|
897
|
+
socket = new LocalWebSocketImpl(localUrl, {
|
|
898
|
+
headers: consoleProxyHeaders(owner),
|
|
899
|
+
perMessageDeflate: false
|
|
900
|
+
});
|
|
724
901
|
} catch (error) {
|
|
725
902
|
sendLifecycleControl(owner, {
|
|
726
903
|
type: 'ws-error',
|
|
@@ -871,6 +1048,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
871
1048
|
|
|
872
1049
|
const handleDataChannelMessage = (owner, channelState, raw) => {
|
|
873
1050
|
if (!isOwnerActive(owner) || channelState.closed) return;
|
|
1051
|
+
if (!ownerWorkspaceAccessCurrent(owner)) {
|
|
1052
|
+
retirePeer(owner, 'console-workspace-access-expired');
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
874
1055
|
try {
|
|
875
1056
|
if (typeof raw === 'string') throw new Error('console-direct-unframed-message');
|
|
876
1057
|
const message = channelState.assembler.push(raw);
|
|
@@ -991,12 +1172,33 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
991
1172
|
const connectionId = String(payload?.connectionId || '');
|
|
992
1173
|
const ownerHubEpoch = String(payload?.hubEpoch || '');
|
|
993
1174
|
const description = payload?.description;
|
|
1175
|
+
const workspaceAccess = consoleWorkspaceAccess(payload);
|
|
994
1176
|
if (!UUID_PATTERN.test(consoleId)
|
|
995
1177
|
|| !UUID_PATTERN.test(connectionId)
|
|
996
1178
|
|| ownerHubEpoch !== hubEpoch
|
|
1179
|
+
|| !workspaceAccess
|
|
997
1180
|
|| description?.type !== 'offer'
|
|
998
1181
|
|| typeof description.sdp !== 'string'
|
|
999
1182
|
|| byteLength(description.sdp) > SDP_MAX_BYTES) {
|
|
1183
|
+
if (UUID_PATTERN.test(consoleId) && UUID_PATTERN.test(connectionId)) {
|
|
1184
|
+
sendSignal({
|
|
1185
|
+
type: 'rtc-close',
|
|
1186
|
+
consoleId,
|
|
1187
|
+
connectionId,
|
|
1188
|
+
hubEpoch: ownerHubEpoch,
|
|
1189
|
+
reason: workspaceAccess ? 'console-direct-offer-invalid' : 'console-workspace-access-invalid'
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
if (memberIsFenced(workspaceAccess)) {
|
|
1195
|
+
sendSignal({
|
|
1196
|
+
type: 'rtc-close',
|
|
1197
|
+
consoleId,
|
|
1198
|
+
connectionId,
|
|
1199
|
+
hubEpoch: ownerHubEpoch,
|
|
1200
|
+
reason: 'workspace-member-revoked'
|
|
1201
|
+
});
|
|
1000
1202
|
return null;
|
|
1001
1203
|
}
|
|
1002
1204
|
if (candidateIsRelay(description.sdp)) {
|
|
@@ -1062,6 +1264,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1062
1264
|
hubEpoch: ownerHubEpoch,
|
|
1063
1265
|
generation: ++peerGeneration,
|
|
1064
1266
|
peer,
|
|
1267
|
+
workspaceAccess,
|
|
1065
1268
|
control: null,
|
|
1066
1269
|
channels: new Map(),
|
|
1067
1270
|
pendingLogicalControl: new Map(),
|
|
@@ -1069,12 +1272,14 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1069
1272
|
wireBudget: createDirectConsoleWireBudget({ maxBytes: MAX_SHARED_ASSEMBLY_BYTES }),
|
|
1070
1273
|
iceTimer: null,
|
|
1071
1274
|
disconnectTimer: null,
|
|
1275
|
+
accessTimer: null,
|
|
1072
1276
|
peerState: 'new',
|
|
1073
1277
|
iceState: 'new',
|
|
1074
1278
|
retired: false,
|
|
1075
1279
|
createdAt: Date.now()
|
|
1076
1280
|
};
|
|
1077
1281
|
peers.set(consoleId, owner);
|
|
1282
|
+
armOwnerAccessDeadline(owner);
|
|
1078
1283
|
peer.onLocalDescription((sdp, type) => {
|
|
1079
1284
|
if (!isOwnerActive(owner) || String(type).toLowerCase() !== 'answer' || byteLength(sdp) > SDP_MAX_BYTES) return;
|
|
1080
1285
|
if (candidateIsRelay(sdp)) {
|
|
@@ -1160,10 +1365,17 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1160
1365
|
if (!payload?.type) return;
|
|
1161
1366
|
if (payload.type === 'signal-ready' && payload.role === 'hub') {
|
|
1162
1367
|
const nextEpoch = String(payload.hubEpoch || '');
|
|
1368
|
+
const signaledWorkspaceId = String(payload.workspaceId || '').trim();
|
|
1369
|
+
const expectedWorkspaceId = currentHubWorkspaceAccess().workspaceId;
|
|
1163
1370
|
if (!UUID_PATTERN.test(nextEpoch)) {
|
|
1164
1371
|
lastError = 'console-direct-hub-epoch-invalid';
|
|
1165
1372
|
return;
|
|
1166
1373
|
}
|
|
1374
|
+
if (expectedWorkspaceId && signaledWorkspaceId !== expectedWorkspaceId) {
|
|
1375
|
+
lastError = 'console-direct-workspace-mismatch';
|
|
1376
|
+
try { socket?.close?.(1008, 'console-direct-workspace-mismatch'); } catch {}
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1167
1379
|
clearSignalReadyDeadline(ownerSignalGeneration, socket);
|
|
1168
1380
|
if (hubEpoch && hubEpoch !== nextEpoch) retireAllPeers('console-direct-hub-epoch-replaced');
|
|
1169
1381
|
hubEpoch = nextEpoch;
|
|
@@ -1174,8 +1386,85 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1174
1386
|
retryAttempt = 0;
|
|
1175
1387
|
retryNotBeforeAt = 0;
|
|
1176
1388
|
nextRetryAt = '';
|
|
1389
|
+
armSignalHeartbeat(ownerSignalGeneration, socket, connect);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (payload.type === 'workspace-access-expired') {
|
|
1393
|
+
retireAllPeers('console-workspace-access-expired');
|
|
1394
|
+
lastError = 'console-workspace-access-expired';
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (payload.type === 'workspace-access-refreshed') {
|
|
1398
|
+
const consoleId = String(payload.consoleId || '');
|
|
1399
|
+
const connectionId = String(payload.connectionId || '');
|
|
1400
|
+
const owner = peers.get(consoleId);
|
|
1401
|
+
const refreshedAccess = consoleWorkspaceAccess(payload);
|
|
1402
|
+
if (!owner
|
|
1403
|
+
|| owner.connectionId !== connectionId
|
|
1404
|
+
|| owner.hubEpoch !== String(payload.hubEpoch || '')
|
|
1405
|
+
|| !refreshedAccess
|
|
1406
|
+
|| owner.workspaceAccess.workspaceId !== refreshedAccess.workspaceId
|
|
1407
|
+
|| owner.workspaceAccess.userId !== refreshedAccess.userId
|
|
1408
|
+
|| memberIsFenced(refreshedAccess)) {
|
|
1409
|
+
if (owner) retirePeer(owner, 'console-workspace-access-refresh-invalid');
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
owner.workspaceAccess = refreshedAccess;
|
|
1413
|
+
armOwnerAccessDeadline(owner);
|
|
1177
1414
|
return;
|
|
1178
1415
|
}
|
|
1416
|
+
if (payload.type === 'workspace-reauth-required') {
|
|
1417
|
+
const workspaceId = String(payload.workspaceId || '').trim();
|
|
1418
|
+
const challengeNonce = String(payload.challengeNonce || '').trim();
|
|
1419
|
+
if (workspaceId !== currentHubWorkspaceAccess().workspaceId
|
|
1420
|
+
|| !UUID_PATTERN.test(challengeNonce)
|
|
1421
|
+
|| !(workspaceReauthUrl instanceof URL)) {
|
|
1422
|
+
lastError = 'console-workspace-reauth-challenge-invalid';
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
if (workspaceReauthAttempt?.challengeNonce === challengeNonce) return;
|
|
1426
|
+
try { workspaceReauthAttempt?.controller?.abort?.(); } catch {}
|
|
1427
|
+
if (workspaceReauthAttempt?.timer) clearTimeout(workspaceReauthAttempt.timer);
|
|
1428
|
+
const controller = new AbortController();
|
|
1429
|
+
const attempt = {
|
|
1430
|
+
challengeNonce,
|
|
1431
|
+
generation: ownerSignalGeneration,
|
|
1432
|
+
socket,
|
|
1433
|
+
controller,
|
|
1434
|
+
timer: null
|
|
1435
|
+
};
|
|
1436
|
+
workspaceReauthAttempt = attempt;
|
|
1437
|
+
attempt.timer = setTimeout(() => controller.abort(), accessTokenTimeoutMs);
|
|
1438
|
+
attempt.timer.unref?.();
|
|
1439
|
+
void readAccessToken(ownerSignalGeneration).then(async result => {
|
|
1440
|
+
if (signalSocket !== socket
|
|
1441
|
+
|| ownerSignalGeneration !== signalGeneration
|
|
1442
|
+
|| result.kind !== 'value') return;
|
|
1443
|
+
const accessToken = String(result.value || '').trim();
|
|
1444
|
+
if (!accessToken) return;
|
|
1445
|
+
try {
|
|
1446
|
+
const response = await fetchImpl(workspaceReauthUrl.href, {
|
|
1447
|
+
method: 'POST',
|
|
1448
|
+
headers: {
|
|
1449
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1450
|
+
'Content-Type': 'application/json',
|
|
1451
|
+
Accept: 'application/json'
|
|
1452
|
+
},
|
|
1453
|
+
body: JSON.stringify({ workspaceId, challengeNonce }),
|
|
1454
|
+
signal: controller.signal
|
|
1455
|
+
});
|
|
1456
|
+
if (!response?.ok) lastError = `console-workspace-reauth-failed:${Number(response?.status || 0)}`;
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
if (error?.name !== 'AbortError') lastError = 'console-workspace-reauth-failed';
|
|
1459
|
+
}
|
|
1460
|
+
}).finally(() => {
|
|
1461
|
+
if (workspaceReauthAttempt !== attempt) return;
|
|
1462
|
+
if (attempt.timer) clearTimeout(attempt.timer);
|
|
1463
|
+
workspaceReauthAttempt = null;
|
|
1464
|
+
});
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (payload.type === 'workspace-reauth-ok') return;
|
|
1179
1468
|
const consoleId = String(payload.consoleId || '');
|
|
1180
1469
|
const connectionId = String(payload.connectionId || '');
|
|
1181
1470
|
const ownerHubEpoch = String(payload.hubEpoch || '');
|
|
@@ -1284,6 +1573,114 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1284
1573
|
signalReadyDeadline = deadline;
|
|
1285
1574
|
};
|
|
1286
1575
|
|
|
1576
|
+
const clearSignalHeartbeat = (ownerSignalGeneration, socket) => {
|
|
1577
|
+
const heartbeat = signalHeartbeat;
|
|
1578
|
+
if (!heartbeat
|
|
1579
|
+
|| (ownerSignalGeneration !== undefined && heartbeat.generation !== ownerSignalGeneration)
|
|
1580
|
+
|| (socket !== undefined && heartbeat.socket !== socket)) {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
signalHeartbeat = null;
|
|
1584
|
+
cancelSignalHeartbeatTimer(heartbeat.timer);
|
|
1585
|
+
return true;
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
const failSignalHeartbeat = (heartbeat, connect, reason) => {
|
|
1589
|
+
if (signalHeartbeat !== heartbeat
|
|
1590
|
+
|| stopped
|
|
1591
|
+
|| heartbeat.generation !== signalGeneration
|
|
1592
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
clearSignalHeartbeat(heartbeat.generation, heartbeat.socket);
|
|
1596
|
+
signalSocket = null;
|
|
1597
|
+
lastHttpStatus = 0;
|
|
1598
|
+
lastError = reason;
|
|
1599
|
+
retireNegotiatingPeers(reason);
|
|
1600
|
+
try { heartbeat.socket.terminate?.(); } catch {}
|
|
1601
|
+
scheduleReconnect(connect);
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
const scheduleSignalHeartbeatPing = (heartbeat, connect) => {
|
|
1605
|
+
if (signalHeartbeat !== heartbeat
|
|
1606
|
+
|| stopped
|
|
1607
|
+
|| heartbeat.generation !== signalGeneration
|
|
1608
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
heartbeat.awaitingPong = false;
|
|
1612
|
+
let pingTimer = null;
|
|
1613
|
+
pingTimer = scheduleSignalHeartbeatTimer(() => {
|
|
1614
|
+
if (signalHeartbeat !== heartbeat
|
|
1615
|
+
|| heartbeat.timer !== pingTimer
|
|
1616
|
+
|| stopped
|
|
1617
|
+
|| heartbeat.generation !== signalGeneration
|
|
1618
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
heartbeat.awaitingPong = true;
|
|
1622
|
+
let timeoutTimer = null;
|
|
1623
|
+
timeoutTimer = scheduleSignalHeartbeatTimer(() => {
|
|
1624
|
+
if (signalHeartbeat !== heartbeat
|
|
1625
|
+
|| heartbeat.timer !== timeoutTimer
|
|
1626
|
+
|| !heartbeat.awaitingPong
|
|
1627
|
+
|| stopped
|
|
1628
|
+
|| heartbeat.generation !== signalGeneration
|
|
1629
|
+
|| signalSocket !== heartbeat.socket) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
failSignalHeartbeat(
|
|
1633
|
+
heartbeat,
|
|
1634
|
+
connect,
|
|
1635
|
+
'console-direct-signal-heartbeat-timeout'
|
|
1636
|
+
);
|
|
1637
|
+
}, signalHeartbeatTimeoutMs);
|
|
1638
|
+
heartbeat.timer = timeoutTimer;
|
|
1639
|
+
heartbeat.timer.unref?.();
|
|
1640
|
+
try {
|
|
1641
|
+
if (heartbeat.socket.readyState !== WebSocket.OPEN
|
|
1642
|
+
|| typeof heartbeat.socket.ping !== 'function') {
|
|
1643
|
+
throw new Error('console-direct-signal-heartbeat-unavailable');
|
|
1644
|
+
}
|
|
1645
|
+
heartbeat.socket.ping();
|
|
1646
|
+
} catch {
|
|
1647
|
+
failSignalHeartbeat(
|
|
1648
|
+
heartbeat,
|
|
1649
|
+
connect,
|
|
1650
|
+
'console-direct-signal-heartbeat-failed'
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
}, signalHeartbeatIntervalMs);
|
|
1654
|
+
heartbeat.timer = pingTimer;
|
|
1655
|
+
heartbeat.timer.unref?.();
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
const armSignalHeartbeat = (ownerSignalGeneration, socket, connect) => {
|
|
1659
|
+
clearSignalHeartbeat();
|
|
1660
|
+
const heartbeat = {
|
|
1661
|
+
generation: ownerSignalGeneration,
|
|
1662
|
+
socket,
|
|
1663
|
+
timer: null,
|
|
1664
|
+
awaitingPong: false
|
|
1665
|
+
};
|
|
1666
|
+
signalHeartbeat = heartbeat;
|
|
1667
|
+
scheduleSignalHeartbeatPing(heartbeat, connect);
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
const acceptSignalHeartbeatPong = (ownerSignalGeneration, socket, connect) => {
|
|
1671
|
+
const heartbeat = signalHeartbeat;
|
|
1672
|
+
if (!heartbeat
|
|
1673
|
+
|| heartbeat.generation !== ownerSignalGeneration
|
|
1674
|
+
|| heartbeat.socket !== socket
|
|
1675
|
+
|| !heartbeat.awaitingPong) {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
cancelSignalHeartbeatTimer(heartbeat.timer);
|
|
1679
|
+
heartbeat.timer = null;
|
|
1680
|
+
scheduleSignalHeartbeatPing(heartbeat, connect);
|
|
1681
|
+
return true;
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1287
1684
|
const scheduleReconnect = connect => {
|
|
1288
1685
|
if (stopped || retryTimer || !(signalUrl instanceof URL)) return;
|
|
1289
1686
|
const sequenceDelay = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
|
|
@@ -1329,6 +1726,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1329
1726
|
const url = new URL(signalUrl);
|
|
1330
1727
|
url.searchParams.set('deviceId', deviceId);
|
|
1331
1728
|
url.searchParams.set('hubInstanceId', hubInstanceId);
|
|
1729
|
+
const hubWorkspaceAccess = currentHubWorkspaceAccess();
|
|
1730
|
+
if (hubWorkspaceAccess.workspaceId) {
|
|
1731
|
+
url.searchParams.set('workspaceId', hubWorkspaceAccess.workspaceId);
|
|
1732
|
+
}
|
|
1332
1733
|
let socket;
|
|
1333
1734
|
try {
|
|
1334
1735
|
socket = new SignalingWebSocketImpl(url, {
|
|
@@ -1374,12 +1775,16 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1374
1775
|
handleSignalMessage(raw, ownerSignalGeneration, socket);
|
|
1375
1776
|
}
|
|
1376
1777
|
});
|
|
1778
|
+
socket.on('pong', () => {
|
|
1779
|
+
acceptSignalHeartbeatPong(ownerSignalGeneration, socket, connect);
|
|
1780
|
+
});
|
|
1377
1781
|
socket.once('close', () => {
|
|
1378
1782
|
const wasOwner = signalSocket === socket
|
|
1379
1783
|
&& ownerSignalGeneration === signalGeneration
|
|
1380
1784
|
&& !stopped;
|
|
1381
1785
|
if (signalSocket === socket) signalSocket = null;
|
|
1382
1786
|
clearSignalReadyDeadline(ownerSignalGeneration, socket);
|
|
1787
|
+
clearSignalHeartbeat(ownerSignalGeneration, socket);
|
|
1383
1788
|
if (!wasOwner) return;
|
|
1384
1789
|
retireNegotiatingPeers('console-direct-signaling-closed');
|
|
1385
1790
|
if (!retryTimer) state = 'disconnected';
|
|
@@ -1393,6 +1798,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1393
1798
|
}
|
|
1394
1799
|
signalSocket = null;
|
|
1395
1800
|
clearSignalReadyDeadline(ownerSignalGeneration, socket);
|
|
1801
|
+
clearSignalHeartbeat(ownerSignalGeneration, socket);
|
|
1396
1802
|
retireNegotiatingPeers('console-direct-signaling-error');
|
|
1397
1803
|
try { socket.terminate?.(); } catch {}
|
|
1398
1804
|
scheduleReconnect(connect);
|
|
@@ -1411,8 +1817,12 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1411
1817
|
return;
|
|
1412
1818
|
}
|
|
1413
1819
|
signalGeneration += 1;
|
|
1820
|
+
try { workspaceReauthAttempt?.controller?.abort?.(); } catch {}
|
|
1821
|
+
if (workspaceReauthAttempt?.timer) clearTimeout(workspaceReauthAttempt.timer);
|
|
1822
|
+
workspaceReauthAttempt = null;
|
|
1414
1823
|
cancelAccessTokenAttempt();
|
|
1415
1824
|
clearSignalReadyDeadline();
|
|
1825
|
+
clearSignalHeartbeat();
|
|
1416
1826
|
if (retryTimer) clearTimeout(retryTimer);
|
|
1417
1827
|
retryTimer = null;
|
|
1418
1828
|
retryAttempt = 0;
|
|
@@ -1429,8 +1839,12 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1429
1839
|
if (stopped) return;
|
|
1430
1840
|
stopped = true;
|
|
1431
1841
|
signalGeneration += 1;
|
|
1842
|
+
try { workspaceReauthAttempt?.controller?.abort?.(); } catch {}
|
|
1843
|
+
if (workspaceReauthAttempt?.timer) clearTimeout(workspaceReauthAttempt.timer);
|
|
1844
|
+
workspaceReauthAttempt = null;
|
|
1432
1845
|
cancelAccessTokenAttempt();
|
|
1433
1846
|
clearSignalReadyDeadline();
|
|
1847
|
+
clearSignalHeartbeat();
|
|
1434
1848
|
if (retryTimer) clearTimeout(retryTimer);
|
|
1435
1849
|
retryTimer = null;
|
|
1436
1850
|
nextRetryAt = '';
|
|
@@ -1443,6 +1857,31 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1443
1857
|
state = signalUrl ? 'closed' : 'disabled';
|
|
1444
1858
|
};
|
|
1445
1859
|
|
|
1860
|
+
const invalidateWorkspaceAccess = (reason = 'console-workspace-access-invalidated') => {
|
|
1861
|
+
retireAllPeers(String(reason || 'console-workspace-access-invalidated').slice(0, 120));
|
|
1862
|
+
refresh();
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
const revokeWorkspaceMember = (workspaceId, userId, reason = 'workspace-member-revoked') => {
|
|
1866
|
+
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
1867
|
+
const normalizedUserId = String(userId || '').trim();
|
|
1868
|
+
if (!normalizedWorkspaceId || !normalizedUserId) return 0;
|
|
1869
|
+
pruneRevokedMembers();
|
|
1870
|
+
revokedMembers.set(
|
|
1871
|
+
memberFenceKey(normalizedWorkspaceId, normalizedUserId),
|
|
1872
|
+
Date.now() + REVOKED_MEMBER_FENCE_MS
|
|
1873
|
+
);
|
|
1874
|
+
pruneRevokedMembers();
|
|
1875
|
+
let retired = 0;
|
|
1876
|
+
for (const owner of [...peers.values()]) {
|
|
1877
|
+
if (owner.workspaceAccess?.workspaceId !== normalizedWorkspaceId
|
|
1878
|
+
|| owner.workspaceAccess?.userId !== normalizedUserId) continue;
|
|
1879
|
+
retirePeer(owner, String(reason || 'workspace-member-revoked').slice(0, 120));
|
|
1880
|
+
retired += 1;
|
|
1881
|
+
}
|
|
1882
|
+
return retired;
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1446
1885
|
const inspect = () => {
|
|
1447
1886
|
let logicalWebSocketChannels = 0;
|
|
1448
1887
|
let localWebSocketChannels = 0;
|
|
@@ -1454,12 +1893,14 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1454
1893
|
let iceConnectTimers = 0;
|
|
1455
1894
|
let peerDisconnectTimers = 0;
|
|
1456
1895
|
let assemblyDeadlineTimers = 0;
|
|
1896
|
+
let workspaceAccessTimers = 0;
|
|
1457
1897
|
for (const owner of peers.values()) {
|
|
1458
1898
|
logicalWebSocketChannels += owner.channels.size;
|
|
1459
1899
|
pendingHttpRequests += owner.pendingHttp.size;
|
|
1460
1900
|
pendingLogicalControlRequests += owner.pendingLogicalControl.size;
|
|
1461
1901
|
if (owner.iceTimer) iceConnectTimers += 1;
|
|
1462
1902
|
if (owner.disconnectTimer) peerDisconnectTimers += 1;
|
|
1903
|
+
if (owner.accessTimer) workspaceAccessTimers += 1;
|
|
1463
1904
|
if (owner.control && !owner.control.closed) controlChannels += 1;
|
|
1464
1905
|
if (owner.control?.assemblyTimer) assemblyDeadlineTimers += 1;
|
|
1465
1906
|
for (const channelState of owner.channels.values()) {
|
|
@@ -1472,6 +1913,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1472
1913
|
const reconnectTimerActive = Boolean(retryTimer);
|
|
1473
1914
|
const accessTokenDeadlineActive = Boolean(accessTokenAttempt);
|
|
1474
1915
|
const signalReadyDeadlineActive = Boolean(signalReadyDeadline);
|
|
1916
|
+
const signalHeartbeatTimerActive = Boolean(signalHeartbeat?.timer);
|
|
1475
1917
|
return Object.freeze({
|
|
1476
1918
|
enabled: signalUrl instanceof URL && Boolean(deviceId),
|
|
1477
1919
|
state,
|
|
@@ -1488,9 +1930,13 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1488
1930
|
iceConnectTimers,
|
|
1489
1931
|
peerDisconnectTimers,
|
|
1490
1932
|
assemblyDeadlineTimers,
|
|
1933
|
+
workspaceAccessTimers,
|
|
1934
|
+
revokedMemberFences: (pruneRevokedMembers(), revokedMembers.size),
|
|
1491
1935
|
reconnectTimerActive,
|
|
1492
1936
|
accessTokenDeadlineActive,
|
|
1493
1937
|
signalReadyDeadlineActive,
|
|
1938
|
+
signalHeartbeatTimerActive,
|
|
1939
|
+
signalHeartbeatAwaitingPong: signalHeartbeat?.awaitingPong === true,
|
|
1494
1940
|
resourceTimers: iceConnectTimers
|
|
1495
1941
|
+ peerDisconnectTimers
|
|
1496
1942
|
+ assemblyDeadlineTimers
|
|
@@ -1498,7 +1944,10 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1498
1944
|
+ pendingLogicalControlRequests
|
|
1499
1945
|
+ Number(reconnectTimerActive)
|
|
1500
1946
|
+ Number(accessTokenDeadlineActive)
|
|
1501
|
-
+
|
|
1947
|
+
+ workspaceAccessTimers
|
|
1948
|
+
+ Number(Boolean(workspaceReauthAttempt?.timer))
|
|
1949
|
+
+ Number(signalReadyDeadlineActive)
|
|
1950
|
+
+ Number(signalHeartbeatTimerActive),
|
|
1502
1951
|
rejectedRelayCandidates,
|
|
1503
1952
|
dataChannelBackpressureCloses,
|
|
1504
1953
|
malformedWireCloses,
|
|
@@ -1511,7 +1960,14 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1511
1960
|
});
|
|
1512
1961
|
};
|
|
1513
1962
|
|
|
1514
|
-
return Object.freeze({
|
|
1963
|
+
return Object.freeze({
|
|
1964
|
+
start,
|
|
1965
|
+
refresh,
|
|
1966
|
+
close,
|
|
1967
|
+
invalidateWorkspaceAccess,
|
|
1968
|
+
revokeWorkspaceMember,
|
|
1969
|
+
inspect
|
|
1970
|
+
});
|
|
1515
1971
|
}
|
|
1516
1972
|
|
|
1517
1973
|
export const consoleDirectContract = Object.freeze({
|
|
@@ -1530,6 +1986,8 @@ export const consoleDirectContract = Object.freeze({
|
|
|
1530
1986
|
maxDefaultRetryDelayMs: DEFAULT_RETRY_DELAYS_MS.at(-1),
|
|
1531
1987
|
accessTokenTimeoutMs: ACCESS_TOKEN_TIMEOUT_MS,
|
|
1532
1988
|
signalReadyTimeoutMs: SIGNAL_READY_TIMEOUT_MS,
|
|
1989
|
+
signalHeartbeatIntervalMs: SIGNAL_HEARTBEAT_INTERVAL_MS,
|
|
1990
|
+
signalHeartbeatTimeoutMs: SIGNAL_HEARTBEAT_TIMEOUT_MS,
|
|
1533
1991
|
logicalBindTimeoutMs: LOGICAL_BIND_TIMEOUT_MS,
|
|
1534
1992
|
iceConnectTimeoutMs: ICE_CONNECT_TIMEOUT_MS,
|
|
1535
1993
|
peerDisconnectedTimeoutMs: PEER_DISCONNECTED_TIMEOUT_MS,
|