@livedesk/client 0.1.239 → 0.1.241
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/README.md +20 -20
- package/THIRD_PARTY_NOTICES.md +26 -26
- package/bin/client-version.js +23 -23
- package/bin/livedesk-client-fast.js +22 -22
- package/bin/livedesk-client-node.js +66 -66
- package/bin/livedesk-client-update-bootstrap.cjs +319 -319
- package/bin/livedesk-client.js +1 -1
- package/package.json +6 -6
- package/src/runtime/agent-process-lifecycle.js +860 -860
- package/src/runtime/client-runtime-server.js +10 -10
- package/src/runtime/hub-wake-listener.js +159 -159
- package/src/runtime/linux-video-acceleration.js +390 -390
- package/src/runtime/windows-owned-process-manifest.js +402 -402
- package/tests/client-version.test.mjs +27 -27
|
@@ -1022,9 +1022,9 @@ function isLoopbackRequest(req) {
|
|
|
1022
1022
|
return address === '127.0.0.1' || address === '::1' || address === 'localhost' || !address;
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
-
function isTrustedApiOrigin(value, trustedWebOrigins) {
|
|
1026
|
-
const rawOrigin = normalizeString(value, 300);
|
|
1027
|
-
const origin = normalizeOrigin(rawOrigin);
|
|
1025
|
+
function isTrustedApiOrigin(value, trustedWebOrigins) {
|
|
1026
|
+
const rawOrigin = normalizeString(value, 300);
|
|
1027
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
1028
1028
|
if (!origin) return false;
|
|
1029
1029
|
if (trustedWebOrigins.has(origin)) return true;
|
|
1030
1030
|
try {
|
|
@@ -1048,10 +1048,10 @@ function isTrustedLocalRequest(req, host, port, options = {}) {
|
|
|
1048
1048
|
const requestHost = normalizeString(req.headers.host, 300).split(':')[0].toLowerCase();
|
|
1049
1049
|
if (requestHost && requestHost !== host.toLowerCase() && requestHost !== 'localhost' && requestHost !== '127.0.0.1') return false;
|
|
1050
1050
|
|
|
1051
|
-
// Chromium can mark modulepreload/stylesheet requests as cross-site. Static
|
|
1052
|
-
// assets are not privileged. An opaque Origin (`null`) is never accepted for
|
|
1053
|
-
// an API route because it cannot distinguish the LiveDesk shell from an
|
|
1054
|
-
// attacker-controlled file, sandbox, or data page.
|
|
1051
|
+
// Chromium can mark modulepreload/stylesheet requests as cross-site. Static
|
|
1052
|
+
// assets are not privileged. An opaque Origin (`null`) is never accepted for
|
|
1053
|
+
// an API route because it cannot distinguish the LiveDesk shell from an
|
|
1054
|
+
// attacker-controlled file, sandbox, or data page.
|
|
1055
1055
|
if (options.allowStaticCrossSite === true) {
|
|
1056
1056
|
return true;
|
|
1057
1057
|
}
|
|
@@ -1071,9 +1071,9 @@ function isTrustedLocalRequest(req, host, port, options = {}) {
|
|
|
1071
1071
|
|| fetchSite === 'none';
|
|
1072
1072
|
}
|
|
1073
1073
|
|
|
1074
|
-
function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
|
|
1075
|
-
const origin = normalizeString(requestOrigin, 300);
|
|
1076
|
-
return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
|
|
1074
|
+
function resolveCorsOrigin(requestOrigin, port, trustedWebOrigins) {
|
|
1075
|
+
const origin = normalizeString(requestOrigin, 300);
|
|
1076
|
+
return origin && isTrustedApiOrigin(origin, trustedWebOrigins)
|
|
1077
1077
|
? normalizeOrigin(origin)
|
|
1078
1078
|
: `http://127.0.0.1:${port}`;
|
|
1079
1079
|
}
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
const DEFAULT_HUB_WAKE_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
|
|
2
|
-
const DEFAULT_RETRY_DELAYS_MS = [1000, 2000, 5000, 10_000, 30_000];
|
|
3
|
-
|
|
4
|
-
function normalizeUrl(value) {
|
|
5
|
-
const configured = String(value || DEFAULT_HUB_WAKE_URL).trim();
|
|
6
|
-
if (!configured || /^(off|disabled|none)$/i.test(configured)) return '';
|
|
7
|
-
const url = new URL(configured);
|
|
8
|
-
if (url.protocol === 'http:') url.protocol = 'ws:';
|
|
9
|
-
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
10
|
-
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
11
|
-
throw new Error(`unsupported-hub-wake-protocol:${url.protocol}`);
|
|
12
|
-
}
|
|
13
|
-
url.pathname = '/v1/wait';
|
|
14
|
-
url.search = '';
|
|
15
|
-
url.hash = '';
|
|
16
|
-
return url.toString();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseWakeMessage(value) {
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(String(value || ''));
|
|
22
|
-
return parsed?.type === 'hub-online' ? parsed : null;
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function resolveHubWakeUrl(value = process.env.LIVEDESK_WAKE_URL) {
|
|
29
|
-
return normalizeUrl(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function createHubWakeListener(options = {}) {
|
|
33
|
-
const url = resolveHubWakeUrl(options.url);
|
|
34
|
-
if (!url) return null;
|
|
35
|
-
|
|
36
|
-
const WebSocketImpl = options.WebSocketImpl || (await import('ws')).default;
|
|
37
|
-
const retryDelaysMs = Array.isArray(options.retryDelaysMs) && options.retryDelaysMs.length > 0
|
|
38
|
-
? options.retryDelaysMs.map(value => Math.max(100, Number(value) || 1000))
|
|
39
|
-
: DEFAULT_RETRY_DELAYS_MS;
|
|
40
|
-
const getAccessToken = typeof options.getAccessToken === 'function'
|
|
41
|
-
? options.getAccessToken
|
|
42
|
-
: async () => String(options.accessToken || '').trim();
|
|
43
|
-
let stopped = false;
|
|
44
|
-
let socket = null;
|
|
45
|
-
let retryTimer = null;
|
|
46
|
-
let retryAttempt = 0;
|
|
47
|
-
let connectionGeneration = 0;
|
|
48
|
-
let settle;
|
|
49
|
-
|
|
50
|
-
const promise = new Promise(resolve => {
|
|
51
|
-
settle = resolve;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const closeSocket = current => {
|
|
55
|
-
if (!current) return;
|
|
56
|
-
try {
|
|
57
|
-
current.close();
|
|
58
|
-
} catch {
|
|
59
|
-
// Continue to the exact-socket termination fallback below.
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
if (typeof current.terminate === 'function') current.terminate();
|
|
63
|
-
} catch {
|
|
64
|
-
// The listener is best-effort and polling remains the fallback.
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const close = () => {
|
|
69
|
-
if (stopped) return;
|
|
70
|
-
stopped = true;
|
|
71
|
-
connectionGeneration += 1;
|
|
72
|
-
if (retryTimer) {
|
|
73
|
-
clearTimeout(retryTimer);
|
|
74
|
-
retryTimer = null;
|
|
75
|
-
}
|
|
76
|
-
const current = socket;
|
|
77
|
-
socket = null;
|
|
78
|
-
closeSocket(current);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const scheduleReconnect = connect => {
|
|
82
|
-
if (stopped || retryTimer) return;
|
|
83
|
-
const delayMs = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
|
|
84
|
-
retryAttempt += 1;
|
|
85
|
-
retryTimer = setTimeout(() => {
|
|
86
|
-
retryTimer = null;
|
|
87
|
-
void connect();
|
|
88
|
-
}, delayMs);
|
|
89
|
-
retryTimer.unref?.();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const connect = async () => {
|
|
93
|
-
if (stopped) return;
|
|
94
|
-
const generation = ++connectionGeneration;
|
|
95
|
-
let accessToken = '';
|
|
96
|
-
try {
|
|
97
|
-
accessToken = String(await getAccessToken() || '').trim();
|
|
98
|
-
} catch {
|
|
99
|
-
accessToken = '';
|
|
100
|
-
}
|
|
101
|
-
if (stopped || generation !== connectionGeneration) return;
|
|
102
|
-
if (!accessToken) {
|
|
103
|
-
scheduleReconnect(connect);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let current;
|
|
108
|
-
try {
|
|
109
|
-
current = new WebSocketImpl(url, {
|
|
110
|
-
headers: { Authorization: `Bearer ${accessToken}` }
|
|
111
|
-
});
|
|
112
|
-
if (stopped || generation !== connectionGeneration) {
|
|
113
|
-
closeSocket(current);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
socket = current;
|
|
117
|
-
} catch {
|
|
118
|
-
scheduleReconnect(connect);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
current.once('open', () => {
|
|
123
|
-
if (stopped || generation !== connectionGeneration || socket !== current) {
|
|
124
|
-
closeSocket(current);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
retryAttempt = 0;
|
|
128
|
-
});
|
|
129
|
-
current.on('message', raw => {
|
|
130
|
-
const event = parseWakeMessage(raw);
|
|
131
|
-
if (!event || stopped) return;
|
|
132
|
-
settle?.(event);
|
|
133
|
-
settle = null;
|
|
134
|
-
close();
|
|
135
|
-
});
|
|
136
|
-
current.once('close', () => {
|
|
137
|
-
if (socket === current) socket = null;
|
|
138
|
-
if (generation !== connectionGeneration) return;
|
|
139
|
-
scheduleReconnect(connect);
|
|
140
|
-
});
|
|
141
|
-
current.once('error', () => {
|
|
142
|
-
closeSocket(current);
|
|
143
|
-
if (generation === connectionGeneration) scheduleReconnect(connect);
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
void connect();
|
|
148
|
-
return {
|
|
149
|
-
url,
|
|
150
|
-
promise,
|
|
151
|
-
close,
|
|
152
|
-
inspect: () => ({
|
|
153
|
-
stopped,
|
|
154
|
-
connectionGeneration,
|
|
155
|
-
socketActive: socket !== null,
|
|
156
|
-
retryTimerActive: retryTimer !== null
|
|
157
|
-
})
|
|
158
|
-
};
|
|
159
|
-
}
|
|
1
|
+
const DEFAULT_HUB_WAKE_URL = 'https://livedesk-wake.lovecrdm.workers.dev';
|
|
2
|
+
const DEFAULT_RETRY_DELAYS_MS = [1000, 2000, 5000, 10_000, 30_000];
|
|
3
|
+
|
|
4
|
+
function normalizeUrl(value) {
|
|
5
|
+
const configured = String(value || DEFAULT_HUB_WAKE_URL).trim();
|
|
6
|
+
if (!configured || /^(off|disabled|none)$/i.test(configured)) return '';
|
|
7
|
+
const url = new URL(configured);
|
|
8
|
+
if (url.protocol === 'http:') url.protocol = 'ws:';
|
|
9
|
+
if (url.protocol === 'https:') url.protocol = 'wss:';
|
|
10
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
|
11
|
+
throw new Error(`unsupported-hub-wake-protocol:${url.protocol}`);
|
|
12
|
+
}
|
|
13
|
+
url.pathname = '/v1/wait';
|
|
14
|
+
url.search = '';
|
|
15
|
+
url.hash = '';
|
|
16
|
+
return url.toString();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseWakeMessage(value) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(String(value || ''));
|
|
22
|
+
return parsed?.type === 'hub-online' ? parsed : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveHubWakeUrl(value = process.env.LIVEDESK_WAKE_URL) {
|
|
29
|
+
return normalizeUrl(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createHubWakeListener(options = {}) {
|
|
33
|
+
const url = resolveHubWakeUrl(options.url);
|
|
34
|
+
if (!url) return null;
|
|
35
|
+
|
|
36
|
+
const WebSocketImpl = options.WebSocketImpl || (await import('ws')).default;
|
|
37
|
+
const retryDelaysMs = Array.isArray(options.retryDelaysMs) && options.retryDelaysMs.length > 0
|
|
38
|
+
? options.retryDelaysMs.map(value => Math.max(100, Number(value) || 1000))
|
|
39
|
+
: DEFAULT_RETRY_DELAYS_MS;
|
|
40
|
+
const getAccessToken = typeof options.getAccessToken === 'function'
|
|
41
|
+
? options.getAccessToken
|
|
42
|
+
: async () => String(options.accessToken || '').trim();
|
|
43
|
+
let stopped = false;
|
|
44
|
+
let socket = null;
|
|
45
|
+
let retryTimer = null;
|
|
46
|
+
let retryAttempt = 0;
|
|
47
|
+
let connectionGeneration = 0;
|
|
48
|
+
let settle;
|
|
49
|
+
|
|
50
|
+
const promise = new Promise(resolve => {
|
|
51
|
+
settle = resolve;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const closeSocket = current => {
|
|
55
|
+
if (!current) return;
|
|
56
|
+
try {
|
|
57
|
+
current.close();
|
|
58
|
+
} catch {
|
|
59
|
+
// Continue to the exact-socket termination fallback below.
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if (typeof current.terminate === 'function') current.terminate();
|
|
63
|
+
} catch {
|
|
64
|
+
// The listener is best-effort and polling remains the fallback.
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const close = () => {
|
|
69
|
+
if (stopped) return;
|
|
70
|
+
stopped = true;
|
|
71
|
+
connectionGeneration += 1;
|
|
72
|
+
if (retryTimer) {
|
|
73
|
+
clearTimeout(retryTimer);
|
|
74
|
+
retryTimer = null;
|
|
75
|
+
}
|
|
76
|
+
const current = socket;
|
|
77
|
+
socket = null;
|
|
78
|
+
closeSocket(current);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const scheduleReconnect = connect => {
|
|
82
|
+
if (stopped || retryTimer) return;
|
|
83
|
+
const delayMs = retryDelaysMs[Math.min(retryAttempt, retryDelaysMs.length - 1)];
|
|
84
|
+
retryAttempt += 1;
|
|
85
|
+
retryTimer = setTimeout(() => {
|
|
86
|
+
retryTimer = null;
|
|
87
|
+
void connect();
|
|
88
|
+
}, delayMs);
|
|
89
|
+
retryTimer.unref?.();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const connect = async () => {
|
|
93
|
+
if (stopped) return;
|
|
94
|
+
const generation = ++connectionGeneration;
|
|
95
|
+
let accessToken = '';
|
|
96
|
+
try {
|
|
97
|
+
accessToken = String(await getAccessToken() || '').trim();
|
|
98
|
+
} catch {
|
|
99
|
+
accessToken = '';
|
|
100
|
+
}
|
|
101
|
+
if (stopped || generation !== connectionGeneration) return;
|
|
102
|
+
if (!accessToken) {
|
|
103
|
+
scheduleReconnect(connect);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let current;
|
|
108
|
+
try {
|
|
109
|
+
current = new WebSocketImpl(url, {
|
|
110
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
111
|
+
});
|
|
112
|
+
if (stopped || generation !== connectionGeneration) {
|
|
113
|
+
closeSocket(current);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
socket = current;
|
|
117
|
+
} catch {
|
|
118
|
+
scheduleReconnect(connect);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
current.once('open', () => {
|
|
123
|
+
if (stopped || generation !== connectionGeneration || socket !== current) {
|
|
124
|
+
closeSocket(current);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
retryAttempt = 0;
|
|
128
|
+
});
|
|
129
|
+
current.on('message', raw => {
|
|
130
|
+
const event = parseWakeMessage(raw);
|
|
131
|
+
if (!event || stopped) return;
|
|
132
|
+
settle?.(event);
|
|
133
|
+
settle = null;
|
|
134
|
+
close();
|
|
135
|
+
});
|
|
136
|
+
current.once('close', () => {
|
|
137
|
+
if (socket === current) socket = null;
|
|
138
|
+
if (generation !== connectionGeneration) return;
|
|
139
|
+
scheduleReconnect(connect);
|
|
140
|
+
});
|
|
141
|
+
current.once('error', () => {
|
|
142
|
+
closeSocket(current);
|
|
143
|
+
if (generation === connectionGeneration) scheduleReconnect(connect);
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
void connect();
|
|
148
|
+
return {
|
|
149
|
+
url,
|
|
150
|
+
promise,
|
|
151
|
+
close,
|
|
152
|
+
inspect: () => ({
|
|
153
|
+
stopped,
|
|
154
|
+
connectionGeneration,
|
|
155
|
+
socketActive: socket !== null,
|
|
156
|
+
retryTimerActive: retryTimer !== null
|
|
157
|
+
})
|
|
158
|
+
};
|
|
159
|
+
}
|