@zimic/interceptor 0.17.0-canary.2 → 0.17.0-canary.4

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.
Files changed (60) hide show
  1. package/dist/{chunk-TYHJPU5G.js → chunk-MXHLBRPB.js} +521 -61
  2. package/dist/chunk-MXHLBRPB.js.map +1 -0
  3. package/dist/{chunk-3SKHNQLL.mjs → chunk-OGL76CKO.mjs} +510 -60
  4. package/dist/chunk-OGL76CKO.mjs.map +1 -0
  5. package/dist/cli.js +141 -17
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cli.mjs +137 -13
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/http.d.ts +23 -2
  10. package/dist/http.js +473 -270
  11. package/dist/http.js.map +1 -1
  12. package/dist/http.mjs +473 -270
  13. package/dist/http.mjs.map +1 -1
  14. package/dist/scripts/postinstall.js +6 -6
  15. package/dist/scripts/postinstall.js.map +1 -1
  16. package/dist/scripts/postinstall.mjs +5 -5
  17. package/dist/scripts/postinstall.mjs.map +1 -1
  18. package/dist/server.d.ts +16 -0
  19. package/dist/server.js +6 -6
  20. package/dist/server.mjs +1 -1
  21. package/package.json +12 -11
  22. package/src/cli/browser/init.ts +5 -6
  23. package/src/cli/cli.ts +140 -55
  24. package/src/cli/server/start.ts +22 -7
  25. package/src/cli/server/token/create.ts +33 -0
  26. package/src/cli/server/token/list.ts +23 -0
  27. package/src/cli/server/token/remove.ts +22 -0
  28. package/src/http/interceptor/HttpInterceptorClient.ts +49 -27
  29. package/src/http/interceptor/HttpInterceptorStore.ts +25 -9
  30. package/src/http/interceptor/LocalHttpInterceptor.ts +6 -3
  31. package/src/http/interceptor/RemoteHttpInterceptor.ts +41 -5
  32. package/src/http/interceptor/errors/RunningHttpInterceptorError.ts +1 -1
  33. package/src/http/interceptor/types/options.ts +15 -0
  34. package/src/http/interceptor/types/public.ts +11 -3
  35. package/src/http/interceptorWorker/HttpInterceptorWorker.ts +14 -16
  36. package/src/http/interceptorWorker/RemoteHttpInterceptorWorker.ts +17 -12
  37. package/src/http/interceptorWorker/types/options.ts +1 -0
  38. package/src/http/requestHandler/errors/TimesCheckError.ts +1 -1
  39. package/src/server/InterceptorServer.ts +52 -8
  40. package/src/server/constants.ts +1 -1
  41. package/src/server/errors/InvalidInterceptorTokenError.ts +13 -0
  42. package/src/server/errors/InvalidInterceptorTokenFileError.ts +13 -0
  43. package/src/server/errors/InvalidInterceptorTokenValueError.ts +13 -0
  44. package/src/server/types/options.ts +9 -0
  45. package/src/server/types/public.ts +9 -0
  46. package/src/server/types/schema.ts +4 -4
  47. package/src/server/utils/auth.ts +304 -0
  48. package/src/server/utils/fetch.ts +3 -1
  49. package/src/utils/data.ts +13 -0
  50. package/src/utils/files.ts +14 -0
  51. package/src/utils/{console.ts → logging.ts} +5 -7
  52. package/src/utils/webSocket.ts +57 -11
  53. package/src/webSocket/WebSocketClient.ts +11 -5
  54. package/src/webSocket/WebSocketHandler.ts +72 -51
  55. package/src/webSocket/WebSocketServer.ts +25 -4
  56. package/src/webSocket/constants.ts +2 -0
  57. package/src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts +11 -0
  58. package/src/webSocket/types.ts +49 -52
  59. package/dist/chunk-3SKHNQLL.mjs.map +0 -1
  60. package/dist/chunk-TYHJPU5G.js.map +0 -1
@@ -1,5 +1,8 @@
1
1
  import ClientSocket, { type WebSocketServer as ServerSocket } from 'isomorphic-ws';
2
2
 
3
+ import { WebSocketControlMessage } from '@/webSocket/constants';
4
+ import UnauthorizedWebSocketConnectionError from '@/webSocket/errors/UnauthorizedWebSocketConnectionError';
5
+
3
6
  class WebSocketTimeoutError extends Error {}
4
7
 
5
8
  export class WebSocketOpenTimeoutError extends WebSocketTimeoutError {
@@ -37,9 +40,10 @@ export async function waitForOpenClientSocket(
37
40
  socket: ClientSocket,
38
41
  options: {
39
42
  timeout?: number;
43
+ waitForAuthentication?: boolean;
40
44
  } = {},
41
45
  ) {
42
- const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT } = options;
46
+ const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT, waitForAuthentication = false } = options;
43
47
 
44
48
  const isAlreadyOpen = socket.readyState === socket.OPEN;
45
49
 
@@ -48,24 +52,60 @@ export async function waitForOpenClientSocket(
48
52
  }
49
53
 
50
54
  await new Promise<void>((resolve, reject) => {
51
- function handleOpenError(error: unknown) {
55
+ function removeAllSocketListeners() {
56
+ socket.removeEventListener('message', handleSocketMessage); // eslint-disable-line @typescript-eslint/no-use-before-define
52
57
  socket.removeEventListener('open', handleOpenSuccess); // eslint-disable-line @typescript-eslint/no-use-before-define
58
+ socket.removeEventListener('error', handleOpenError); // eslint-disable-line @typescript-eslint/no-use-before-define
59
+ socket.removeEventListener('close', handleClose); // eslint-disable-line @typescript-eslint/no-use-before-define
60
+ }
61
+
62
+ function handleOpenError(error: unknown) {
63
+ removeAllSocketListeners();
53
64
  reject(error);
54
65
  }
55
66
 
67
+ function handleClose(event: ClientSocket.CloseEvent) {
68
+ const isUnauthorized = event.code === 1008;
69
+
70
+ /* istanbul ignore else -- @preserve
71
+ * An unauthorized close event is the only one we expect to happen here. */
72
+ if (isUnauthorized) {
73
+ const unauthorizedError = new UnauthorizedWebSocketConnectionError(event);
74
+ handleOpenError(unauthorizedError);
75
+ } else {
76
+ handleOpenError(event);
77
+ }
78
+ }
79
+
56
80
  const openTimeout = setTimeout(() => {
57
81
  const timeoutError = new WebSocketOpenTimeoutError(timeoutDuration);
58
82
  handleOpenError(timeoutError);
59
83
  }, timeoutDuration);
60
84
 
61
85
  function handleOpenSuccess() {
62
- socket.removeEventListener('error', handleOpenError);
86
+ removeAllSocketListeners();
63
87
  clearTimeout(openTimeout);
64
88
  resolve();
65
89
  }
66
90
 
67
- socket.addEventListener('open', handleOpenSuccess);
91
+ function handleSocketMessage(message: ClientSocket.MessageEvent) {
92
+ const hasValidAuth = message.data === ('socket:auth:valid' satisfies WebSocketControlMessage);
93
+
94
+ /* istanbul ignore else -- @preserve
95
+ * We currently only support the 'socket:auth:valid' message and it is the only possible control message here. */
96
+ if (hasValidAuth) {
97
+ handleOpenSuccess();
98
+ }
99
+ }
100
+
101
+ if (waitForAuthentication) {
102
+ socket.addEventListener('message', handleSocketMessage);
103
+ } else {
104
+ socket.addEventListener('open', handleOpenSuccess);
105
+ }
106
+
68
107
  socket.addEventListener('error', handleOpenError);
108
+ socket.addEventListener('close', handleClose);
69
109
  });
70
110
  }
71
111
 
@@ -78,24 +118,30 @@ export async function closeClientSocket(socket: ClientSocket, options: { timeout
78
118
  }
79
119
 
80
120
  await new Promise<void>((resolve, reject) => {
81
- function handleCloseError(error: unknown) {
82
- socket.removeEventListener('close', handleCloseSuccess); // eslint-disable-line @typescript-eslint/no-use-before-define
121
+ function removeAllSocketListeners() {
122
+ socket.removeEventListener('error', handleError); // eslint-disable-line @typescript-eslint/no-use-before-define
123
+ socket.removeEventListener('close', handleClose); // eslint-disable-line @typescript-eslint/no-use-before-define
124
+ }
125
+
126
+ function handleError(error: unknown) {
127
+ removeAllSocketListeners();
83
128
  reject(error);
84
129
  }
85
130
 
86
131
  const closeTimeout = setTimeout(() => {
87
132
  const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration);
88
- handleCloseError(timeoutError);
133
+ handleError(timeoutError);
89
134
  }, timeoutDuration);
90
135
 
91
- function handleCloseSuccess() {
92
- socket.removeEventListener('error', handleCloseError);
136
+ function handleClose() {
137
+ removeAllSocketListeners();
93
138
  clearTimeout(closeTimeout);
94
139
  resolve();
95
140
  }
96
141
 
97
- socket.addEventListener('error', handleCloseError);
98
- socket.addEventListener('close', handleCloseSuccess);
142
+ socket.addEventListener('error', handleError);
143
+ socket.addEventListener('close', handleClose);
144
+
99
145
  socket.close();
100
146
  });
101
147
  }
@@ -1,7 +1,7 @@
1
1
  import validateURLProtocol from '@zimic/utils/url/validateURLProtocol';
2
2
  import ClientSocket from 'isomorphic-ws';
3
3
 
4
- import { WebSocket } from './types';
4
+ import { WebSocketSchema } from './types';
5
5
  import WebSocketHandler from './WebSocketHandler';
6
6
 
7
7
  const SUPPORTED_WEB_SOCKET_PROTOCOLS = ['ws', 'wss'];
@@ -12,7 +12,7 @@ interface WebSocketClientOptions {
12
12
  messageTimeout?: number;
13
13
  }
14
14
 
15
- class WebSocketClient<Schema extends WebSocket.ServiceSchema> extends WebSocketHandler<Schema> {
15
+ class WebSocketClient<Schema extends WebSocketSchema> extends WebSocketHandler<Schema> {
16
16
  private url: URL;
17
17
 
18
18
  private socket?: ClientSocket;
@@ -31,11 +31,17 @@ class WebSocketClient<Schema extends WebSocket.ServiceSchema> extends WebSocketH
31
31
  return this.socket !== undefined && this.socket.readyState === this.socket.OPEN;
32
32
  }
33
33
 
34
- async start() {
35
- this.socket = new ClientSocket(this.url);
34
+ async start(options: { parameters?: Record<string, string>; waitForAuthentication?: boolean } = {}) {
35
+ const parametersAsString = options.parameters
36
+ ? Object.entries(options.parameters)
37
+ .map(([key, value]) => `${key}=${value}`)
38
+ .map(encodeURIComponent)
39
+ : [];
40
+
41
+ this.socket = new ClientSocket(this.url, parametersAsString);
36
42
 
37
43
  try {
38
- await super.registerSocket(this.socket);
44
+ await super.registerSocket(this.socket, options);
39
45
  } catch (error) {
40
46
  await this.stop();
41
47
  throw error;
@@ -11,20 +11,31 @@ import {
11
11
  waitForOpenClientSocket,
12
12
  } from '@/utils/webSocket';
13
13
 
14
+ import { WEB_SOCKET_CONTROL_MESSAGES, WebSocketControlMessage } from './constants';
14
15
  import InvalidWebSocketMessage from './errors/InvalidWebSocketMessage';
15
16
  import NotRunningWebSocketHandlerError from './errors/NotRunningWebSocketHandlerError';
16
- import { WebSocket } from './types';
17
-
18
- abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
17
+ import {
18
+ WebSocketEventMessageListener,
19
+ WebSocketReplyMessageListener,
20
+ WebSocketReplyMessage,
21
+ WebSocketEventMessage,
22
+ WebSocketSchema,
23
+ WebSocketChannel,
24
+ WebSocketChannelWithReply,
25
+ WebSocketChannelWithNoReply,
26
+ WebSocketMessage,
27
+ } from './types';
28
+
29
+ abstract class WebSocketHandler<Schema extends WebSocketSchema> {
19
30
  private sockets = new Set<ClientSocket>();
20
31
 
21
32
  socketTimeout: number;
22
33
  messageTimeout: number;
23
34
 
24
35
  private channelListeners: {
25
- [Channel in WebSocket.ServiceChannel<Schema>]?: {
26
- event: Set<WebSocket.EventMessageListener<Schema, Channel>>;
27
- reply: Set<WebSocket.ReplyMessageListener<Schema, Channel>>;
36
+ [Channel in WebSocketChannel<Schema>]?: {
37
+ event: Set<WebSocketEventMessageListener<Schema, Channel>>;
38
+ reply: Set<WebSocketReplyMessageListener<Schema, Channel>>;
28
39
  };
29
40
  } = {};
30
41
 
@@ -39,8 +50,11 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
39
50
 
40
51
  abstract isRunning: boolean;
41
52
 
42
- protected async registerSocket(socket: ClientSocket) {
43
- const openPromise = waitForOpenClientSocket(socket, { timeout: this.socketTimeout });
53
+ protected async registerSocket(socket: ClientSocket, options: { waitForAuthentication?: boolean } = {}) {
54
+ const openPromise = waitForOpenClientSocket(socket, {
55
+ timeout: this.socketTimeout,
56
+ waitForAuthentication: options.waitForAuthentication,
57
+ });
44
58
 
45
59
  const handleSocketMessage = async (rawMessage: ClientSocket.MessageEvent) => {
46
60
  await this.handleSocketMessage(socket, rawMessage);
@@ -58,10 +72,12 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
58
72
 
59
73
  const handleSocketClose = () => {
60
74
  socket.removeEventListener('message', handleSocketMessage);
61
- socket.removeEventListener('error', handleSocketError);
62
75
  socket.removeEventListener('close', handleSocketClose);
76
+ socket.removeEventListener('error', handleSocketError);
77
+
63
78
  this.removeSocket(socket);
64
79
  };
80
+
65
81
  socket.addEventListener('close', handleSocketClose);
66
82
 
67
83
  this.sockets.add(socket);
@@ -69,6 +85,10 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
69
85
 
70
86
  private handleSocketMessage = async (socket: ClientSocket, rawMessage: ClientSocket.MessageEvent) => {
71
87
  try {
88
+ if (this.isControlMessageData(rawMessage.data)) {
89
+ return;
90
+ }
91
+
72
92
  const stringifiedMessageData = this.readRawMessageData(rawMessage.data);
73
93
  const parsedMessageData = this.parseMessage(stringifiedMessageData);
74
94
  await this.notifyListeners(parsedMessageData, socket);
@@ -77,6 +97,12 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
77
97
  }
78
98
  };
79
99
 
100
+ private isControlMessageData(messageData: ClientSocket.Data): messageData is WebSocketControlMessage {
101
+ return (
102
+ typeof messageData === 'string' && WEB_SOCKET_CONTROL_MESSAGES.includes(messageData as WebSocketControlMessage)
103
+ );
104
+ }
105
+
80
106
  private readRawMessageData(data: ClientSocket.Data) {
81
107
  /* istanbul ignore else -- @preserve
82
108
  * All supported websocket messages should be encoded as strings. */
@@ -87,7 +113,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
87
113
  }
88
114
  }
89
115
 
90
- private parseMessage(stringifiedMessage: string): WebSocket.ServiceMessage<Schema> {
116
+ private parseMessage(stringifiedMessage: string): WebSocketMessage<Schema> {
91
117
  let parsedMessage: unknown;
92
118
 
93
119
  try {
@@ -96,7 +122,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
96
122
  throw new InvalidWebSocketMessage(stringifiedMessage);
97
123
  }
98
124
 
99
- if (!this.isValidMessage(parsedMessage)) {
125
+ if (!this.isMessage(parsedMessage)) {
100
126
  throw new InvalidWebSocketMessage(stringifiedMessage);
101
127
  }
102
128
 
@@ -116,7 +142,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
116
142
  };
117
143
  }
118
144
 
119
- private isValidMessage(message: unknown): message is WebSocket.ServiceMessage<Schema> {
145
+ private isMessage(message: unknown): message is WebSocketMessage<Schema> {
120
146
  return (
121
147
  typeof message === 'object' &&
122
148
  message !== null &&
@@ -128,7 +154,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
128
154
  );
129
155
  }
130
156
 
131
- private async notifyListeners(message: WebSocket.ServiceMessage<Schema>, socket: ClientSocket) {
157
+ private async notifyListeners(message: WebSocketMessage<Schema>, socket: ClientSocket) {
132
158
  if (this.isReplyMessage(message)) {
133
159
  await this.notifyReplyListeners(message, socket);
134
160
  } else {
@@ -136,7 +162,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
136
162
  }
137
163
  }
138
164
 
139
- private async notifyReplyListeners(message: WebSocket.ServiceReplyMessage<Schema>, socket: ClientSocket) {
165
+ private async notifyReplyListeners(message: WebSocketReplyMessage<Schema>, socket: ClientSocket) {
140
166
  /* istanbul ignore next -- @preserve
141
167
  * Reply listeners are always present when notified in normal conditions. If they were not present, the request
142
168
  * would reach a timeout and not be responded. The empty set serves as a fallback. */
@@ -149,10 +175,7 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
149
175
  await Promise.all(listenerPromises);
150
176
  }
151
177
 
152
- private async notifyEventListeners(
153
- message: WebSocket.ServiceMessage<Schema, WebSocket.ServiceChannel<Schema>>,
154
- socket: ClientSocket,
155
- ) {
178
+ private async notifyEventListeners(message: WebSocketEventMessage<Schema>, socket: ClientSocket) {
156
179
  const listeners = this.channelListeners[message.channel]?.event ?? new Set();
157
180
 
158
181
  const listenerPromises = Array.from(listeners, async (listener) => {
@@ -175,13 +198,13 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
175
198
  this.sockets.delete(socket);
176
199
  }
177
200
 
178
- private async createEventMessage<Channel extends WebSocket.ServiceChannel<Schema>>(
201
+ private async createEventMessage<Channel extends WebSocketChannel<Schema>>(
179
202
  channel: Channel,
180
- eventData: WebSocket.ServiceEventMessage<Schema, Channel>['data'],
203
+ eventData: WebSocketEventMessage<Schema, Channel>['data'],
181
204
  ) {
182
205
  const crypto = await importCrypto();
183
206
 
184
- const eventMessage: WebSocket.ServiceEventMessage<Schema, Channel> = {
207
+ const eventMessage: WebSocketEventMessage<Schema, Channel> = {
185
208
  id: crypto.randomUUID(),
186
209
  channel,
187
210
  data: eventData,
@@ -189,9 +212,9 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
189
212
  return eventMessage;
190
213
  }
191
214
 
192
- async send<Channel extends WebSocket.EventWithNoReplyServiceChannel<Schema>>(
215
+ async send<Channel extends WebSocketChannelWithNoReply<Schema>>(
193
216
  channel: Channel,
194
- eventData: WebSocket.ServiceEventMessage<Schema, Channel>['data'],
217
+ eventData: WebSocketEventMessage<Schema, Channel>['data'],
195
218
  options: {
196
219
  sockets?: Collection<ClientSocket>;
197
220
  } = {},
@@ -200,9 +223,9 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
200
223
  this.sendMessage(event, options.sockets);
201
224
  }
202
225
 
203
- async request<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
226
+ async request<Channel extends WebSocketChannelWithReply<Schema>>(
204
227
  channel: Channel,
205
- requestData: WebSocket.ServiceEventMessage<Schema, Channel>['data'],
228
+ requestData: WebSocketEventMessage<Schema, Channel>['data'],
206
229
  options: {
207
230
  sockets?: Collection<ClientSocket>;
208
231
  } = {},
@@ -214,12 +237,12 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
214
237
  return response.data;
215
238
  }
216
239
 
217
- async waitForReply<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
240
+ async waitForReply<Channel extends WebSocketChannelWithReply<Schema>>(
218
241
  channel: Channel,
219
- requestId: WebSocket.ServiceEventMessage<Schema, Channel>['id'],
242
+ requestId: WebSocketEventMessage<Schema, Channel>['id'],
220
243
  sockets: Collection<ClientSocket> = this.sockets,
221
244
  ) {
222
- return new Promise<WebSocket.ServiceReplyMessage<Schema, Channel>>((resolve, reject) => {
245
+ return new Promise<WebSocketReplyMessage<Schema, Channel>>((resolve, reject) => {
223
246
  const replyTimeout = setTimeout(() => {
224
247
  this.offReply(channel, replyListener); // eslint-disable-line @typescript-eslint/no-use-before-define
225
248
  this.offAbortSocketMessages(sockets, abortListener); // eslint-disable-line @typescript-eslint/no-use-before-define
@@ -250,15 +273,13 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
250
273
  });
251
274
  }
252
275
 
253
- private isReplyMessage<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
254
- message: WebSocket.ServiceMessage<Schema, Channel>,
255
- ) {
276
+ private isReplyMessage<Channel extends WebSocketChannel<Schema>>(message: WebSocketMessage<Schema, Channel>) {
256
277
  return 'requestId' in message;
257
278
  }
258
279
 
259
- async reply<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
260
- request: WebSocket.ServiceEventMessage<Schema, Channel>,
261
- replyData: WebSocket.ServiceReplyMessage<Schema, Channel>['data'],
280
+ async reply<Channel extends WebSocketChannel<Schema>>(
281
+ request: WebSocketEventMessage<Schema, Channel>,
282
+ replyData: WebSocketReplyMessage<Schema, Channel>['data'],
262
283
  options: {
263
284
  sockets: Collection<ClientSocket>;
264
285
  },
@@ -271,13 +292,13 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
271
292
  }
272
293
  }
273
294
 
274
- private async createReplyMessage<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
275
- request: WebSocket.ServiceEventMessage<Schema, Channel>,
276
- replyData: WebSocket.ServiceReplyMessage<Schema, Channel>['data'],
295
+ private async createReplyMessage<Channel extends WebSocketChannel<Schema>>(
296
+ request: WebSocketEventMessage<Schema, Channel>,
297
+ replyData: WebSocketReplyMessage<Schema, Channel>['data'],
277
298
  ) {
278
299
  const crypto = await importCrypto();
279
300
 
280
- const replyMessage: WebSocket.ServiceReplyMessage<Schema, Channel> = {
301
+ const replyMessage: WebSocketReplyMessage<Schema, Channel> = {
281
302
  id: crypto.randomUUID(),
282
303
  channel: request.channel,
283
304
  requestId: request.id,
@@ -286,8 +307,8 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
286
307
  return replyMessage;
287
308
  }
288
309
 
289
- private sendMessage<Channel extends WebSocket.ServiceChannel<Schema>>(
290
- message: WebSocket.ServiceMessage<Schema, Channel>,
310
+ private sendMessage<Channel extends WebSocketChannel<Schema>>(
311
+ message: WebSocketMessage<Schema, Channel>,
291
312
  sockets: Collection<ClientSocket> = this.sockets,
292
313
  ) {
293
314
  if (!this.isRunning) {
@@ -301,16 +322,16 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
301
322
  }
302
323
  }
303
324
 
304
- onEvent<
305
- Channel extends WebSocket.ServiceChannel<Schema>,
306
- Listener extends WebSocket.EventMessageListener<Schema, Channel>,
307
- >(channel: Channel, listener: Listener): Listener {
325
+ onEvent<Channel extends WebSocketChannel<Schema>, Listener extends WebSocketEventMessageListener<Schema, Channel>>(
326
+ channel: Channel,
327
+ listener: Listener,
328
+ ): Listener {
308
329
  const listeners = this.getOrCreateChannelListeners<Channel>(channel);
309
330
  listeners.event.add(listener);
310
331
  return listener;
311
332
  }
312
333
 
313
- private getOrCreateChannelListeners<Channel extends WebSocket.ServiceChannel<Schema>>(channel: Channel) {
334
+ private getOrCreateChannelListeners<Channel extends WebSocketChannel<Schema>>(channel: Channel) {
314
335
  const listeners = this.channelListeners[channel] ?? {
315
336
  event: new Set(),
316
337
  reply: new Set(),
@@ -324,24 +345,24 @@ abstract class WebSocketHandler<Schema extends WebSocket.ServiceSchema> {
324
345
  }
325
346
 
326
347
  onReply<
327
- Channel extends WebSocket.EventWithReplyServiceChannel<Schema>,
328
- Listener extends WebSocket.ReplyMessageListener<Schema, Channel>,
348
+ Channel extends WebSocketChannelWithReply<Schema>,
349
+ Listener extends WebSocketReplyMessageListener<Schema, Channel>,
329
350
  >(channel: Channel, listener: Listener): Listener {
330
351
  const listeners = this.getOrCreateChannelListeners<Channel>(channel);
331
352
  listeners.reply.add(listener);
332
353
  return listener;
333
354
  }
334
355
 
335
- offEvent<Channel extends WebSocket.ServiceChannel<Schema>>(
356
+ offEvent<Channel extends WebSocketChannel<Schema>>(
336
357
  channel: Channel,
337
- listener: WebSocket.EventMessageListener<Schema, Channel>,
358
+ listener: WebSocketEventMessageListener<Schema, Channel>,
338
359
  ) {
339
360
  this.channelListeners[channel]?.event.delete(listener);
340
361
  }
341
362
 
342
- offReply<Channel extends WebSocket.EventWithReplyServiceChannel<Schema>>(
363
+ offReply<Channel extends WebSocketChannelWithReply<Schema>>(
343
364
  channel: Channel,
344
- listener: WebSocket.ReplyMessageListener<Schema, Channel>,
365
+ listener: WebSocketReplyMessageListener<Schema, Channel>,
345
366
  ) {
346
367
  this.channelListeners[channel]?.reply.delete(listener);
347
368
  }
@@ -1,22 +1,32 @@
1
- import { Server as HttpServer } from 'http';
1
+ import { PossiblePromise } from '@zimic/utils/types';
2
+ import { Server as HttpServer, IncomingMessage } from 'http';
2
3
  import ClientSocket from 'isomorphic-ws';
3
4
 
4
5
  import { closeServerSocket } from '@/utils/webSocket';
5
6
 
6
- import { WebSocket } from './types';
7
+ import { WebSocketControlMessage } from './constants';
8
+ import { WebSocketSchema } from './types';
7
9
  import WebSocketHandler from './WebSocketHandler';
8
10
 
9
11
  const { WebSocketServer: ServerSocket } = ClientSocket;
10
12
 
13
+ export type WebSocketServerAuthenticate = (
14
+ socket: ClientSocket,
15
+ request: IncomingMessage,
16
+ ) => PossiblePromise<{ isValid: true } | { isValid: false; message: string }>;
17
+
11
18
  interface WebSocketServerOptions {
12
19
  httpServer: HttpServer;
13
20
  socketTimeout?: number;
14
21
  messageTimeout?: number;
22
+ authenticate?: WebSocketServerAuthenticate;
15
23
  }
16
24
 
17
- class WebSocketServer<Schema extends WebSocket.ServiceSchema> extends WebSocketHandler<Schema> {
25
+ class WebSocketServer<Schema extends WebSocketSchema> extends WebSocketHandler<Schema> {
18
26
  private webSocketServer?: InstanceType<typeof ServerSocket>;
27
+
19
28
  private httpServer: HttpServer;
29
+ private authenticate?: WebSocketServerOptions['authenticate'];
20
30
 
21
31
  constructor(options: WebSocketServerOptions) {
22
32
  super({
@@ -25,6 +35,7 @@ class WebSocketServer<Schema extends WebSocket.ServiceSchema> extends WebSocketH
25
35
  });
26
36
 
27
37
  this.httpServer = options.httpServer;
38
+ this.authenticate = options.authenticate;
28
39
  }
29
40
 
30
41
  get isRunning() {
@@ -42,9 +53,19 @@ class WebSocketServer<Schema extends WebSocket.ServiceSchema> extends WebSocketH
42
53
  console.error(error);
43
54
  });
44
55
 
45
- webSocketServer.on('connection', async (socket) => {
56
+ webSocketServer.on('connection', async (socket, request) => {
57
+ if (this.authenticate) {
58
+ const result = await this.authenticate(socket, request);
59
+
60
+ if (!result.isValid) {
61
+ socket.close(1008, result.message);
62
+ return;
63
+ }
64
+ }
65
+
46
66
  try {
47
67
  await super.registerSocket(socket);
68
+ socket.send('socket:auth:valid' satisfies WebSocketControlMessage);
48
69
  } catch (error) {
49
70
  webSocketServer.emit('error', error);
50
71
  }
@@ -0,0 +1,2 @@
1
+ export const WEB_SOCKET_CONTROL_MESSAGES = Object.freeze(['socket:auth:valid'] as const);
2
+ export type WebSocketControlMessage = (typeof WEB_SOCKET_CONTROL_MESSAGES)[number];
@@ -0,0 +1,11 @@
1
+ import { CloseEvent } from 'isomorphic-ws';
2
+
3
+ /** Error thrown when the connection to the interceptor web socket server is unauthorized. */
4
+ class UnauthorizedWebSocketConnectionError extends Error {
5
+ constructor(readonly event: CloseEvent) {
6
+ super(`${event.reason} (code ${event.code})`);
7
+ this.name = 'UnauthorizedWebSocketConnectionError';
8
+ }
9
+ }
10
+
11
+ export default UnauthorizedWebSocketConnectionError;
@@ -2,68 +2,65 @@ import { JSONSerialized, JSONValue } from '@zimic/http';
2
2
  import { PossiblePromise } from '@zimic/utils/types';
3
3
  import type { WebSocket as ClientSocket } from 'isomorphic-ws';
4
4
 
5
- export namespace WebSocket {
6
- export interface EventMessage<Data extends JSONValue.Loose = JSONValue> {
7
- id: string;
8
- channel: string;
9
- data: Data;
10
- }
5
+ export interface WebSocketEventMessage<
6
+ Schema extends WebSocketSchema,
7
+ Channel extends WebSocketChannel<Schema> = WebSocketChannel<Schema>,
8
+ > {
9
+ id: string;
10
+ channel: Channel;
11
+ data: Schema[Channel]['event'];
12
+ }
11
13
 
12
- export interface ReplyMessage<Data extends JSONValue.Loose = JSONValue> extends EventMessage<Data> {
13
- requestId: string;
14
- }
14
+ export interface WebSocketReplyMessage<
15
+ Schema extends WebSocketSchema,
16
+ Channel extends WebSocketChannel<Schema> = WebSocketChannel<Schema>,
17
+ > {
18
+ id: string;
19
+ requestId: string;
20
+ channel: Channel;
21
+ data: Schema[Channel]['reply'];
22
+ }
15
23
 
16
- export type Message<Data extends JSONValue.Loose = JSONValue> = EventMessage<Data> | ReplyMessage<Data>;
24
+ export type WebSocketMessage<
25
+ Schema extends WebSocketSchema,
26
+ Channel extends WebSocketChannel<Schema> = WebSocketChannel<Schema>,
27
+ > = WebSocketEventMessage<Schema, Channel> | WebSocketReplyMessage<Schema, Channel>;
17
28
 
18
- interface ServiceSchemaDefinition {
19
- [channel: string]: {
20
- event?: JSONValue.Loose;
21
- reply?: JSONValue.Loose;
22
- };
23
- }
29
+ interface BaseWebSocketSchema {
30
+ [channel: string]: {
31
+ event?: JSONValue.Loose;
32
+ reply?: JSONValue.Loose;
33
+ };
34
+ }
35
+
36
+ export type WebSocketSchema<Schema extends BaseWebSocketSchema = BaseWebSocketSchema> =
37
+ WebSocketSchema.ConvertToStrict<Schema>;
24
38
 
25
- type ConvertToStrictServiceSchema<Schema extends ServiceSchemaDefinition> = {
39
+ export namespace WebSocketSchema {
40
+ export type ConvertToStrict<Schema extends BaseWebSocketSchema> = {
26
41
  [Channel in keyof Schema]: {
27
42
  [Key in keyof Schema[Channel]]: JSONSerialized<Schema[Channel][Key]>;
28
43
  };
29
44
  };
45
+ }
30
46
 
31
- export type ServiceSchema<Schema extends ServiceSchemaDefinition = ServiceSchemaDefinition> =
32
- ConvertToStrictServiceSchema<Schema>;
33
-
34
- export type ServiceChannel<Schema extends ServiceSchema> = keyof Schema & string;
35
-
36
- export type EventWithNoReplyServiceChannel<Schema extends ServiceSchema> = {
37
- [Channel in ServiceChannel<Schema>]: Schema[Channel]['reply'] extends JSONValue ? never : Channel;
38
- }[ServiceChannel<Schema>];
39
-
40
- export type EventWithReplyServiceChannel<Schema extends ServiceSchema> = Exclude<
41
- ServiceChannel<Schema>,
42
- EventWithNoReplyServiceChannel<Schema>
43
- >;
44
-
45
- export type ServiceEventMessage<
46
- Schema extends ServiceSchema,
47
- Channel extends ServiceChannel<Schema> = ServiceChannel<Schema>,
48
- > = EventMessage<Schema[Channel]['event']>;
47
+ export type WebSocketChannel<Schema extends WebSocketSchema> = keyof Schema & string;
49
48
 
50
- export type ServiceReplyMessage<
51
- Schema extends ServiceSchema,
52
- Channel extends ServiceChannel<Schema> = ServiceChannel<Schema>,
53
- > = ReplyMessage<Schema[Channel]['reply']>;
49
+ export type WebSocketChannelWithNoReply<Schema extends WebSocketSchema> = {
50
+ [Channel in WebSocketChannel<Schema>]: Schema[Channel]['reply'] extends JSONValue ? never : Channel;
51
+ }[WebSocketChannel<Schema>];
54
52
 
55
- export type ServiceMessage<
56
- Schema extends ServiceSchema,
57
- Channel extends ServiceChannel<Schema> = ServiceChannel<Schema>,
58
- > = ServiceEventMessage<Schema, Channel> | ServiceReplyMessage<Schema, Channel>;
53
+ export type WebSocketChannelWithReply<Schema extends WebSocketSchema> = Exclude<
54
+ WebSocketChannel<Schema>,
55
+ WebSocketChannelWithNoReply<Schema>
56
+ >;
59
57
 
60
- export type EventMessageListener<Schema extends ServiceSchema, Channel extends ServiceChannel<Schema>> = (
61
- message: ServiceEventMessage<Schema, Channel>,
62
- socket: ClientSocket,
63
- ) => PossiblePromise<ServiceReplyMessage<Schema, Channel>['data']>;
58
+ export type WebSocketEventMessageListener<Schema extends WebSocketSchema, Channel extends WebSocketChannel<Schema>> = (
59
+ message: WebSocketEventMessage<Schema, Channel>,
60
+ socket: ClientSocket,
61
+ ) => PossiblePromise<WebSocketReplyMessage<Schema, Channel>['data']>;
64
62
 
65
- export type ReplyMessageListener<Schema extends ServiceSchema, Channel extends ServiceChannel<Schema>> = (
66
- message: ServiceReplyMessage<Schema, Channel>,
67
- socket: ClientSocket,
68
- ) => PossiblePromise<void>;
69
- }
63
+ export type WebSocketReplyMessageListener<Schema extends WebSocketSchema, Channel extends WebSocketChannel<Schema>> = (
64
+ message: WebSocketReplyMessage<Schema, Channel>,
65
+ socket: ClientSocket,
66
+ ) => PossiblePromise<void>;