@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.
@@ -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
+ }