@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.
- package/dist/{chunk-TYHJPU5G.js → chunk-MXHLBRPB.js} +521 -61
- package/dist/chunk-MXHLBRPB.js.map +1 -0
- package/dist/{chunk-3SKHNQLL.mjs → chunk-OGL76CKO.mjs} +510 -60
- package/dist/chunk-OGL76CKO.mjs.map +1 -0
- package/dist/cli.js +141 -17
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +137 -13
- package/dist/cli.mjs.map +1 -1
- package/dist/http.d.ts +23 -2
- package/dist/http.js +473 -270
- package/dist/http.js.map +1 -1
- package/dist/http.mjs +473 -270
- package/dist/http.mjs.map +1 -1
- package/dist/scripts/postinstall.js +6 -6
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/postinstall.mjs +5 -5
- package/dist/scripts/postinstall.mjs.map +1 -1
- package/dist/server.d.ts +16 -0
- package/dist/server.js +6 -6
- package/dist/server.mjs +1 -1
- package/package.json +12 -11
- package/src/cli/browser/init.ts +5 -6
- package/src/cli/cli.ts +140 -55
- package/src/cli/server/start.ts +22 -7
- package/src/cli/server/token/create.ts +33 -0
- package/src/cli/server/token/list.ts +23 -0
- package/src/cli/server/token/remove.ts +22 -0
- package/src/http/interceptor/HttpInterceptorClient.ts +49 -27
- package/src/http/interceptor/HttpInterceptorStore.ts +25 -9
- package/src/http/interceptor/LocalHttpInterceptor.ts +6 -3
- package/src/http/interceptor/RemoteHttpInterceptor.ts +41 -5
- package/src/http/interceptor/errors/RunningHttpInterceptorError.ts +1 -1
- package/src/http/interceptor/types/options.ts +15 -0
- package/src/http/interceptor/types/public.ts +11 -3
- package/src/http/interceptorWorker/HttpInterceptorWorker.ts +14 -16
- package/src/http/interceptorWorker/RemoteHttpInterceptorWorker.ts +17 -12
- package/src/http/interceptorWorker/types/options.ts +1 -0
- package/src/http/requestHandler/errors/TimesCheckError.ts +1 -1
- package/src/server/InterceptorServer.ts +52 -8
- package/src/server/constants.ts +1 -1
- package/src/server/errors/InvalidInterceptorTokenError.ts +13 -0
- package/src/server/errors/InvalidInterceptorTokenFileError.ts +13 -0
- package/src/server/errors/InvalidInterceptorTokenValueError.ts +13 -0
- package/src/server/types/options.ts +9 -0
- package/src/server/types/public.ts +9 -0
- package/src/server/types/schema.ts +4 -4
- package/src/server/utils/auth.ts +304 -0
- package/src/server/utils/fetch.ts +3 -1
- package/src/utils/data.ts +13 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/{console.ts → logging.ts} +5 -7
- package/src/utils/webSocket.ts +57 -11
- package/src/webSocket/WebSocketClient.ts +11 -5
- package/src/webSocket/WebSocketHandler.ts +72 -51
- package/src/webSocket/WebSocketServer.ts +25 -4
- package/src/webSocket/constants.ts +2 -0
- package/src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts +11 -0
- package/src/webSocket/types.ts +49 -52
- package/dist/chunk-3SKHNQLL.mjs.map +0 -1
- package/dist/chunk-TYHJPU5G.js.map +0 -1
package/src/utils/webSocket.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
86
|
+
removeAllSocketListeners();
|
|
63
87
|
clearTimeout(openTimeout);
|
|
64
88
|
resolve();
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
|
|
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
|
|
82
|
-
socket.removeEventListener('
|
|
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
|
-
|
|
133
|
+
handleError(timeoutError);
|
|
89
134
|
}, timeoutDuration);
|
|
90
135
|
|
|
91
|
-
function
|
|
92
|
-
|
|
136
|
+
function handleClose() {
|
|
137
|
+
removeAllSocketListeners();
|
|
93
138
|
clearTimeout(closeTimeout);
|
|
94
139
|
resolve();
|
|
95
140
|
}
|
|
96
141
|
|
|
97
|
-
socket.addEventListener('error',
|
|
98
|
-
socket.addEventListener('close',
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
26
|
-
event: Set<
|
|
27
|
-
reply: Set<
|
|
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, {
|
|
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):
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
201
|
+
private async createEventMessage<Channel extends WebSocketChannel<Schema>>(
|
|
179
202
|
channel: Channel,
|
|
180
|
-
eventData:
|
|
203
|
+
eventData: WebSocketEventMessage<Schema, Channel>['data'],
|
|
181
204
|
) {
|
|
182
205
|
const crypto = await importCrypto();
|
|
183
206
|
|
|
184
|
-
const eventMessage:
|
|
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
|
|
215
|
+
async send<Channel extends WebSocketChannelWithNoReply<Schema>>(
|
|
193
216
|
channel: Channel,
|
|
194
|
-
eventData:
|
|
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
|
|
226
|
+
async request<Channel extends WebSocketChannelWithReply<Schema>>(
|
|
204
227
|
channel: Channel,
|
|
205
|
-
requestData:
|
|
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
|
|
240
|
+
async waitForReply<Channel extends WebSocketChannelWithReply<Schema>>(
|
|
218
241
|
channel: Channel,
|
|
219
|
-
requestId:
|
|
242
|
+
requestId: WebSocketEventMessage<Schema, Channel>['id'],
|
|
220
243
|
sockets: Collection<ClientSocket> = this.sockets,
|
|
221
244
|
) {
|
|
222
|
-
return new Promise<
|
|
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
|
|
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
|
|
260
|
-
request:
|
|
261
|
-
replyData:
|
|
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
|
|
275
|
-
request:
|
|
276
|
-
replyData:
|
|
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:
|
|
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
|
|
290
|
-
message:
|
|
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
|
|
306
|
-
Listener
|
|
307
|
-
|
|
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
|
|
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
|
|
328
|
-
Listener extends
|
|
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
|
|
356
|
+
offEvent<Channel extends WebSocketChannel<Schema>>(
|
|
336
357
|
channel: Channel,
|
|
337
|
-
listener:
|
|
358
|
+
listener: WebSocketEventMessageListener<Schema, Channel>,
|
|
338
359
|
) {
|
|
339
360
|
this.channelListeners[channel]?.event.delete(listener);
|
|
340
361
|
}
|
|
341
362
|
|
|
342
|
-
offReply<Channel extends
|
|
363
|
+
offReply<Channel extends WebSocketChannelWithReply<Schema>>(
|
|
343
364
|
channel: Channel,
|
|
344
|
-
listener:
|
|
365
|
+
listener: WebSocketReplyMessageListener<Schema, Channel>,
|
|
345
366
|
) {
|
|
346
367
|
this.channelListeners[channel]?.reply.delete(listener);
|
|
347
368
|
}
|
|
@@ -1,22 +1,32 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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,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;
|
package/src/webSocket/types.ts
CHANGED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
export type WebSocketChannelWithReply<Schema extends WebSocketSchema> = Exclude<
|
|
54
|
+
WebSocketChannel<Schema>,
|
|
55
|
+
WebSocketChannelWithNoReply<Schema>
|
|
56
|
+
>;
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
63
|
+
export type WebSocketReplyMessageListener<Schema extends WebSocketSchema, Channel extends WebSocketChannel<Schema>> = (
|
|
64
|
+
message: WebSocketReplyMessage<Schema, Channel>,
|
|
65
|
+
socket: ClientSocket,
|
|
66
|
+
) => PossiblePromise<void>;
|