@livedesk/hub 0.1.66 → 0.1.68
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/captures/capture-store.js +2 -2
- package/src/console-direct.js +60 -5
- package/src/console-direct.test.mjs +95 -1
- package/src/remote-hub.js +320 -97
- package/src/server.js +272 -30
- package/src/shared-wall-profile-contract.mjs +69 -0
- package/src/shared-wall-profile-contract.test.mjs +99 -0
- package/src/{live-desk-update.js → vuvodesk-update.js} +1 -1
- package/src/wall-source-restart-runtime.test.mjs +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.68",
|
|
4
4
|
"description": "VuvoDesk local Hub API and browser frame bridge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"dev": "node src/server.js",
|
|
13
13
|
"start": "node src/server.js",
|
|
14
14
|
"check": "node --check src/server.js && node --check src/remote-hub.js && node --check src/remote-clipboard-contract.mjs && node --check src/console-direct.js",
|
|
15
|
-
"prepublishOnly": "node ../../scripts/
|
|
15
|
+
"prepublishOnly": "node ../../scripts/vuvodesk-release-git-gate.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
19
|
-
"@livedesk/runtime-core": "0.1.
|
|
19
|
+
"@livedesk/runtime-core": "0.1.9",
|
|
20
20
|
"@openai/codex-sdk": "0.145.0",
|
|
21
21
|
"cors": "^2.8.5",
|
|
22
22
|
"express": "^4.21.2",
|
|
@@ -86,7 +86,7 @@ export class CaptureStore {
|
|
|
86
86
|
throw Object.assign(new Error('capture-mime-type-unsupported'), { status: 415 });
|
|
87
87
|
}
|
|
88
88
|
const extension = MIME_EXTENSIONS.get(mimeType) || (kind === 'screenshot' ? '.png' : '.webm');
|
|
89
|
-
const fileNameBase = safeName(metadata.fileName, `
|
|
89
|
+
const fileNameBase = safeName(metadata.fileName, `VuvoDesk_${kind}`);
|
|
90
90
|
const fileName = fileNameBase.toLowerCase().endsWith(extension) ? fileNameBase : `${fileNameBase}${extension}`;
|
|
91
91
|
const id = randomUUID();
|
|
92
92
|
const folder = kind === 'screenshot' ? 'images' : kind === 'timelapse' ? 'timelapses' : 'recordings';
|
|
@@ -122,7 +122,7 @@ export class CaptureStore {
|
|
|
122
122
|
const sessionId = randomUUID();
|
|
123
123
|
const tempPath = path.join(this.root, '.tmp', `${sessionId}.webm.part`);
|
|
124
124
|
await writeFile(tempPath, Buffer.alloc(0), { mode: 0o600 });
|
|
125
|
-
const fileNameBase = safeName(metadata.fileName, `
|
|
125
|
+
const fileNameBase = safeName(metadata.fileName, `VuvoDesk_${kind}`);
|
|
126
126
|
const session = {
|
|
127
127
|
sessionId,
|
|
128
128
|
kind,
|
package/src/console-direct.js
CHANGED
|
@@ -29,6 +29,7 @@ const MAX_SHARED_ASSEMBLY_BYTES = 12 * 1024 * 1024;
|
|
|
29
29
|
const MAX_FRAME_MESSAGE_BYTES = (8 * 1024 * 1024) + DIRECT_CONSOLE_WIRE_CHUNK_BYTES;
|
|
30
30
|
const MAX_PENDING_MEDIA_WIRE_MESSAGES = 8;
|
|
31
31
|
const MAX_DATA_CHANNEL_BUFFERED_BYTES = 12 * 1024 * 1024;
|
|
32
|
+
const CONTROL_HEADROOM_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
32
33
|
const MAX_LOCAL_WS_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
33
34
|
const HTTP_TIMEOUT_MS = 15_000;
|
|
34
35
|
const ICE_CONNECT_TIMEOUT_MS = 10_000;
|
|
@@ -169,6 +170,7 @@ function allowedHttpRequest(method, pathname) {
|
|
|
169
170
|
|| pathname === '/api/auth/session'
|
|
170
171
|
|| pathname === '/api/remote/status'
|
|
171
172
|
|| pathname === '/api/hub/status'
|
|
173
|
+
|| pathname === '/api/remote/pwa-diagnostics'
|
|
172
174
|
|| pathname === '/api/update/status'
|
|
173
175
|
|| pathname === '/api/update/apply') {
|
|
174
176
|
return true;
|
|
@@ -369,6 +371,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
369
371
|
let lastHttpStatus = 0;
|
|
370
372
|
let rejectedRelayCandidates = 0;
|
|
371
373
|
let dataChannelBackpressureCloses = 0;
|
|
374
|
+
let dataChannelSendFailures = 0;
|
|
375
|
+
let lastDataChannelSendFailure = null;
|
|
372
376
|
let malformedWireCloses = 0;
|
|
373
377
|
let wireBudgetCleanupFailures = 0;
|
|
374
378
|
|
|
@@ -651,7 +655,7 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
651
655
|
|| channelBufferedAmount(right) - channelBufferedAmount(left)
|
|
652
656
|
));
|
|
653
657
|
for (const state of candidates) {
|
|
654
|
-
if (ownerBufferedAmount(owner) + requiredBytes <=
|
|
658
|
+
if (ownerBufferedAmount(owner) + requiredBytes <= CONTROL_HEADROOM_BUFFERED_BYTES) break;
|
|
655
659
|
dataChannelBackpressureCloses += 1;
|
|
656
660
|
closeLogicalChannel(owner, state, {
|
|
657
661
|
code: 1013,
|
|
@@ -680,8 +684,39 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
680
684
|
return false;
|
|
681
685
|
}
|
|
682
686
|
const wireBytes = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
687
|
+
const partialMedia = channelState.purpose === 'frame' || channelState.purpose === 'atlas';
|
|
688
|
+
if (partialMedia
|
|
689
|
+
&& (wireBytes > MAX_DATA_CHANNEL_BUFFERED_BYTES
|
|
690
|
+
|| ownerBufferedAmount(owner) + wireBytes > MAX_DATA_CHANNEL_BUFFERED_BYTES)) {
|
|
691
|
+
dataChannelBackpressureCloses += 1;
|
|
692
|
+
if (options.closeOnFailure !== false) {
|
|
693
|
+
closeLogicalChannel(owner, channelState, {
|
|
694
|
+
code: 1013,
|
|
695
|
+
reason: 'console-direct-datachannel-backpressure',
|
|
696
|
+
notify: false
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
const mediaBufferedBytes = partialMedia ? channelBufferedAmount(channelState) : 0;
|
|
702
|
+
if (partialMedia
|
|
703
|
+
&& mediaBufferedBytes > 0
|
|
704
|
+
&& mediaBufferedBytes + wireBytes > CONTROL_HEADROOM_BUFFERED_BYTES) {
|
|
705
|
+
// Retire only the media lane that owns the stale SCTP backlog. Its
|
|
706
|
+
// browser socket recovery requests a current key frame while the
|
|
707
|
+
// reliable peer and every healthy lane keep their own owners.
|
|
708
|
+
dataChannelBackpressureCloses += 1;
|
|
709
|
+
if (options.closeOnFailure !== false) {
|
|
710
|
+
closeLogicalChannel(owner, channelState, {
|
|
711
|
+
code: 1013,
|
|
712
|
+
reason: 'console-direct-media-headroom',
|
|
713
|
+
notify: false
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
683
718
|
if (channelState === owner.control
|
|
684
|
-
&& ownerBufferedAmount(owner) + wireBytes >
|
|
719
|
+
&& ownerBufferedAmount(owner) + wireBytes > CONTROL_HEADROOM_BUFFERED_BYTES) {
|
|
685
720
|
releaseLogicalBacklogForControl(owner, wireBytes);
|
|
686
721
|
}
|
|
687
722
|
if (wireBytes > MAX_DATA_CHANNEL_BUFFERED_BYTES
|
|
@@ -697,11 +732,28 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
697
732
|
}
|
|
698
733
|
return false;
|
|
699
734
|
}
|
|
700
|
-
for (const chunk of chunks) {
|
|
735
|
+
for (const [chunkIndex, chunk] of chunks.entries()) {
|
|
701
736
|
if (!isOwnerActive(owner) || channelState.closed || !channelIsOpen(channelState.channel)) return false;
|
|
702
737
|
try {
|
|
703
|
-
|
|
704
|
-
|
|
738
|
+
// Normalize the node-datachannel native boundary to its documented
|
|
739
|
+
// Node Buffer shape. Share the exact encoded bytes without copying or
|
|
740
|
+
// retaining a second application queue.
|
|
741
|
+
const nativeChunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
742
|
+
if (channelState.channel.sendMessageBinary(nativeChunk) !== true) throw new Error('send-rejected');
|
|
743
|
+
} catch (error) {
|
|
744
|
+
dataChannelSendFailures += 1;
|
|
745
|
+
lastDataChannelSendFailure = Object.freeze({
|
|
746
|
+
purpose: channelState.purpose,
|
|
747
|
+
channelId: channelState.channelId,
|
|
748
|
+
generation: owner.generation,
|
|
749
|
+
messageId: (channelState.nextSendMessageId - 1) >>> 0,
|
|
750
|
+
chunkIndex,
|
|
751
|
+
chunkCount: chunks.length,
|
|
752
|
+
chunkBytes: chunk.byteLength,
|
|
753
|
+
bufferedBytes: channelBufferedAmount(channelState),
|
|
754
|
+
error: String(error instanceof Error ? error.message : error).slice(0, 120),
|
|
755
|
+
occurredAt: Date.now()
|
|
756
|
+
});
|
|
705
757
|
if (options.closeOnFailure !== false) {
|
|
706
758
|
if (channelState === owner.control) retirePeer(owner, 'console-direct-control-send-failed');
|
|
707
759
|
else closeLogicalChannel(owner, channelState, {
|
|
@@ -1950,6 +2002,8 @@ export function createHubConsoleDirect(options = {}) {
|
|
|
1950
2002
|
+ Number(signalHeartbeatTimerActive),
|
|
1951
2003
|
rejectedRelayCandidates,
|
|
1952
2004
|
dataChannelBackpressureCloses,
|
|
2005
|
+
dataChannelSendFailures,
|
|
2006
|
+
lastDataChannelSendFailure,
|
|
1953
2007
|
malformedWireCloses,
|
|
1954
2008
|
wireBudgetCleanupFailures,
|
|
1955
2009
|
lastConnectedAt,
|
|
@@ -1982,6 +2036,7 @@ export const consoleDirectContract = Object.freeze({
|
|
|
1982
2036
|
maxFrameMessageBytes: MAX_FRAME_MESSAGE_BYTES,
|
|
1983
2037
|
maxPendingMediaWireMessages: MAX_PENDING_MEDIA_WIRE_MESSAGES,
|
|
1984
2038
|
maxBufferedBytes: MAX_DATA_CHANNEL_BUFFERED_BYTES,
|
|
2039
|
+
controlHeadroomBufferedBytes: CONTROL_HEADROOM_BUFFERED_BYTES,
|
|
1985
2040
|
maxLocalWebSocketBufferedBytes: MAX_LOCAL_WS_BUFFERED_BYTES,
|
|
1986
2041
|
maxDefaultRetryDelayMs: DEFAULT_RETRY_DELAYS_MS.at(-1),
|
|
1987
2042
|
accessTokenTimeoutMs: ACCESS_TOKEN_TIMEOUT_MS,
|
|
@@ -23,6 +23,7 @@ const CONSOLE_ID = '22222222-2222-4222-8222-222222222222';
|
|
|
23
23
|
const CONNECTION_ONE = '33333333-3333-4333-8333-333333333333';
|
|
24
24
|
const CONNECTION_TWO = '44444444-4444-4444-8444-444444444444';
|
|
25
25
|
const CHANNEL_ID = '55555555-5555-4555-8555-555555555555';
|
|
26
|
+
const CHANNEL_ID_TWO = '55555555-5555-4555-8555-555555555556';
|
|
26
27
|
const REQUEST_ID = '66666666-6666-4666-8666-666666666666';
|
|
27
28
|
const TEAM_WORKSPACE_ID = '88888888-8888-4888-8888-888888888888';
|
|
28
29
|
const TEAM_OWNER_USER_ID = '99999999-9999-4999-8999-999999999999';
|
|
@@ -123,6 +124,7 @@ class FakeDataChannel {
|
|
|
123
124
|
|
|
124
125
|
sendMessageBinary(value) {
|
|
125
126
|
if (!this.isOpen()) return false;
|
|
127
|
+
if (!Buffer.isBuffer(value)) throw new TypeError('Buffer expected');
|
|
126
128
|
this.sent.push(new Uint8Array(value));
|
|
127
129
|
return true;
|
|
128
130
|
}
|
|
@@ -627,6 +629,48 @@ test('reliable control channel bridges bounded HTTP over fragmented wire message
|
|
|
627
629
|
direct.close();
|
|
628
630
|
});
|
|
629
631
|
|
|
632
|
+
test('reliable control channel admits the authenticated PWA diagnostic route', async () => {
|
|
633
|
+
let fetchCalls = 0;
|
|
634
|
+
const diagnosticBody = JSON.stringify({ schema: 'livedesk.pwa-runtime-diagnostics.v1', events: [] });
|
|
635
|
+
const { direct, signal } = await connectedDirect({
|
|
636
|
+
fetchImpl: async (url, init) => {
|
|
637
|
+
fetchCalls += 1;
|
|
638
|
+
assert.equal(url, 'http://127.0.0.1:5179/api/remote/pwa-diagnostics');
|
|
639
|
+
assert.equal(init.method, 'POST');
|
|
640
|
+
assert.equal(init.headers['content-type'], 'application/json');
|
|
641
|
+
assert.equal(Buffer.from(init.body).toString('utf8'), diagnosticBody);
|
|
642
|
+
return new Response('{"ok":true,"accepted":true}', {
|
|
643
|
+
status: 202,
|
|
644
|
+
statusText: 'Accepted',
|
|
645
|
+
headers: { 'Content-Type': 'application/json' }
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
const peer = offer(signal);
|
|
650
|
+
const control = new FakeDataChannel('livedesk-control-v1');
|
|
651
|
+
peer.emitDataChannel(control);
|
|
652
|
+
control.open();
|
|
653
|
+
|
|
654
|
+
sendWire(control, {
|
|
655
|
+
type: 'http-request',
|
|
656
|
+
requestId: REQUEST_ID,
|
|
657
|
+
method: 'POST',
|
|
658
|
+
path: '/api/remote/pwa-diagnostics',
|
|
659
|
+
headers: { 'content-type': 'application/json' },
|
|
660
|
+
bodyBase64: Buffer.from(diagnosticBody).toString('base64')
|
|
661
|
+
});
|
|
662
|
+
await settle();
|
|
663
|
+
const response = decodeWireMessages(control.sent)
|
|
664
|
+
.map(message => JSON.parse(message.data))
|
|
665
|
+
.find(message => message.type === 'http-response');
|
|
666
|
+
assert.equal(fetchCalls, 1, 'The PWA diagnostic request must reach the local Hub API.');
|
|
667
|
+
assert.equal(response.requestId, REQUEST_ID);
|
|
668
|
+
assert.equal(response.status, 202);
|
|
669
|
+
assert.equal(Buffer.from(response.bodyBase64, 'base64').toString('utf8'), '{"ok":true,"accepted":true}');
|
|
670
|
+
assert.equal(direct.inspect().pendingHttpRequests, 0);
|
|
671
|
+
direct.close();
|
|
672
|
+
});
|
|
673
|
+
|
|
630
674
|
test('Team Operator can use Wall and Control but cannot mutate the Hub owner runtime', async () => {
|
|
631
675
|
const teamWorkspaceAccess = {
|
|
632
676
|
workspaceId: TEAM_WORKSPACE_ID,
|
|
@@ -1184,6 +1228,56 @@ test('the peer-wide DataChannel buffer cap closes only the overflowing logical l
|
|
|
1184
1228
|
direct.close();
|
|
1185
1229
|
});
|
|
1186
1230
|
|
|
1231
|
+
test('media pressure retires only its stale logical lane while preserving the control peer and a healthy lane', async () => {
|
|
1232
|
+
const { direct, signal } = await connectedDirect();
|
|
1233
|
+
const peer = offer(signal);
|
|
1234
|
+
const control = new FakeDataChannel('livedesk-control-v1');
|
|
1235
|
+
peer.emitDataChannel(control);
|
|
1236
|
+
control.open();
|
|
1237
|
+
const lane = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID}:frame`);
|
|
1238
|
+
peer.emitDataChannel(lane);
|
|
1239
|
+
lane.open();
|
|
1240
|
+
sendWire(control, {
|
|
1241
|
+
type: 'ws-open',
|
|
1242
|
+
connectionId: CONNECTION_ONE,
|
|
1243
|
+
hubEpoch: HUB_EPOCH,
|
|
1244
|
+
channelId: CHANNEL_ID,
|
|
1245
|
+
purpose: 'frame',
|
|
1246
|
+
path: '/api/remote/frames/ws'
|
|
1247
|
+
});
|
|
1248
|
+
const local = FakeLocalSocket.instances[0];
|
|
1249
|
+
local.open();
|
|
1250
|
+
const healthyLane = new FakeDataChannel(`livedesk-ws-v1:${CHANNEL_ID_TWO}:frame`);
|
|
1251
|
+
peer.emitDataChannel(healthyLane);
|
|
1252
|
+
healthyLane.open();
|
|
1253
|
+
sendWire(control, {
|
|
1254
|
+
type: 'ws-open',
|
|
1255
|
+
connectionId: CONNECTION_ONE,
|
|
1256
|
+
hubEpoch: HUB_EPOCH,
|
|
1257
|
+
channelId: CHANNEL_ID_TWO,
|
|
1258
|
+
purpose: 'frame',
|
|
1259
|
+
path: '/api/remote/frames/ws'
|
|
1260
|
+
});
|
|
1261
|
+
const healthyLocal = FakeLocalSocket.instances[1];
|
|
1262
|
+
healthyLocal.open();
|
|
1263
|
+
lane.buffered = consoleDirectContract.controlHeadroomBufferedBytes;
|
|
1264
|
+
|
|
1265
|
+
healthyLocal.receive('healthy-frame');
|
|
1266
|
+
assert.ok(healthyLane.sent.length > 0);
|
|
1267
|
+
assert.equal(healthyLane.closed, false);
|
|
1268
|
+
assert.equal(lane.closed, false);
|
|
1269
|
+
|
|
1270
|
+
local.receive('newest-frame');
|
|
1271
|
+
|
|
1272
|
+
assert.equal(lane.sent.length, 0);
|
|
1273
|
+
assert.equal(lane.closed, true);
|
|
1274
|
+
assert.equal(healthyLane.closed, false);
|
|
1275
|
+
assert.equal(control.closed, false);
|
|
1276
|
+
assert.equal(peer.closed, false);
|
|
1277
|
+
assert.equal(direct.inspect().dataChannelBackpressureCloses, 1);
|
|
1278
|
+
direct.close();
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1187
1281
|
test('a reliable control response drops stale media backlog without retiring the peer', async () => {
|
|
1188
1282
|
const { direct, signal } = await connectedDirect({
|
|
1189
1283
|
fetchImpl: async () => new Response('{"online":true}', {
|
|
@@ -1405,7 +1499,7 @@ test('native node-datachannel peers carry the fragmented control wire over loopb
|
|
|
1405
1499
|
kind: 'control'
|
|
1406
1500
|
});
|
|
1407
1501
|
assert.ok(chunks.length > 1);
|
|
1408
|
-
for (const chunk of chunks) assert.equal(offerChannel.sendMessageBinary(chunk), true);
|
|
1502
|
+
for (const chunk of chunks) assert.equal(offerChannel.sendMessageBinary(Buffer.from(chunk)), true);
|
|
1409
1503
|
const message = await received;
|
|
1410
1504
|
clearTimeout(messageTimer);
|
|
1411
1505
|
messageTimer = null;
|