@zimic/interceptor 0.17.0-canary.1 → 0.17.0-canary.3

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 (58) hide show
  1. package/dist/{chunk-3SKHNQLL.mjs → chunk-L75WKVZO.mjs} +508 -60
  2. package/dist/chunk-L75WKVZO.mjs.map +1 -0
  3. package/dist/{chunk-TYHJPU5G.js → chunk-PURXNE6R.js} +519 -61
  4. package/dist/chunk-PURXNE6R.js.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 +14 -0
  10. package/dist/http.js +449 -269
  11. package/dist/http.js.map +1 -1
  12. package/dist/http.mjs +449 -269
  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 +11 -10
  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 +9 -4
  32. package/src/http/interceptor/types/options.ts +15 -0
  33. package/src/http/interceptorWorker/HttpInterceptorWorker.ts +14 -16
  34. package/src/http/interceptorWorker/RemoteHttpInterceptorWorker.ts +17 -12
  35. package/src/http/interceptorWorker/types/options.ts +1 -0
  36. package/src/http/requestHandler/errors/TimesCheckError.ts +1 -1
  37. package/src/server/InterceptorServer.ts +52 -8
  38. package/src/server/constants.ts +1 -1
  39. package/src/server/errors/InvalidInterceptorTokenError.ts +13 -0
  40. package/src/server/errors/InvalidInterceptorTokenFileError.ts +13 -0
  41. package/src/server/errors/InvalidInterceptorTokenValueError.ts +13 -0
  42. package/src/server/types/options.ts +9 -0
  43. package/src/server/types/public.ts +9 -0
  44. package/src/server/types/schema.ts +4 -4
  45. package/src/server/utils/auth.ts +301 -0
  46. package/src/server/utils/fetch.ts +3 -1
  47. package/src/utils/data.ts +13 -0
  48. package/src/utils/files.ts +14 -0
  49. package/src/utils/{console.ts → logging.ts} +5 -7
  50. package/src/utils/webSocket.ts +57 -11
  51. package/src/webSocket/WebSocketClient.ts +11 -5
  52. package/src/webSocket/WebSocketHandler.ts +72 -51
  53. package/src/webSocket/WebSocketServer.ts +25 -4
  54. package/src/webSocket/constants.ts +2 -0
  55. package/src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts +11 -0
  56. package/src/webSocket/types.ts +49 -52
  57. package/dist/chunk-3SKHNQLL.mjs.map +0 -1
  58. package/dist/chunk-TYHJPU5G.js.map +0 -1
@@ -16,7 +16,6 @@ import validateURLProtocol from '@zimic/utils/url/validateURLProtocol';
16
16
  import { isServerSide } from '@/utils/environment';
17
17
 
18
18
  import HttpInterceptorWorker from '../interceptorWorker/HttpInterceptorWorker';
19
- import LocalHttpInterceptorWorker from '../interceptorWorker/LocalHttpInterceptorWorker';
20
19
  import HttpRequestHandlerClient, { AnyHttpRequestHandlerClient } from '../requestHandler/HttpRequestHandlerClient';
21
20
  import LocalHttpRequestHandler from '../requestHandler/LocalHttpRequestHandler';
22
21
  import RemoteHttpRequestHandler from '../requestHandler/RemoteHttpRequestHandler';
@@ -38,11 +37,13 @@ class HttpInterceptorClient<
38
37
  Schema extends HttpSchema,
39
38
  HandlerConstructor extends HttpRequestHandlerConstructor = HttpRequestHandlerConstructor,
40
39
  > {
41
- private worker: HttpInterceptorWorker;
42
40
  private store: HttpInterceptorStore;
43
-
44
41
  private _baseURL!: URL;
45
42
 
43
+ private createWorker: () => HttpInterceptorWorker;
44
+ private deleteWorker: () => void;
45
+ private worker?: HttpInterceptorWorker;
46
+
46
47
  requestSaving: HttpInterceptorRequestSaving;
47
48
  private numberOfSavedRequests = 0;
48
49
 
@@ -67,18 +68,20 @@ class HttpInterceptorClient<
67
68
  };
68
69
 
69
70
  constructor(options: {
70
- worker: HttpInterceptorWorker;
71
71
  store: HttpInterceptorStore;
72
72
  baseURL: URL;
73
+ createWorker: () => HttpInterceptorWorker;
74
+ deleteWorker: () => void;
73
75
  requestSaving?: Partial<HttpInterceptorRequestSaving>;
74
76
  onUnhandledRequest?: UnhandledRequestStrategy;
75
77
  Handler: HandlerConstructor;
76
78
  }) {
77
- this.worker = options.worker;
78
79
  this.store = options.store;
79
-
80
80
  this.baseURL = options.baseURL;
81
81
 
82
+ this.createWorker = options.createWorker;
83
+ this.deleteWorker = options.deleteWorker;
84
+
82
85
  this.requestSaving = {
83
86
  enabled: options.requestSaving?.enabled ?? this.getDefaultRequestSavingEnabled(),
84
87
  safeLimit: options.requestSaving?.safeLimit ?? DEFAULT_REQUEST_SAVING_SAFE_LIMIT,
@@ -108,6 +111,7 @@ class HttpInterceptorClient<
108
111
 
109
112
  validateURLProtocol(newBaseURL, SUPPORTED_BASE_URL_PROTOCOLS);
110
113
  excludeURLParams(newBaseURL);
114
+
111
115
  this._baseURL = newBaseURL;
112
116
  }
113
117
 
@@ -118,29 +122,48 @@ class HttpInterceptorClient<
118
122
  return this.baseURL.href;
119
123
  }
120
124
 
125
+ private get workerOrThrow() {
126
+ if (!this.worker) {
127
+ throw new NotRunningHttpInterceptorError();
128
+ }
129
+ return this.worker;
130
+ }
131
+
121
132
  get platform() {
122
- return this.worker.platform;
133
+ return this.worker?.platform ?? null;
123
134
  }
124
135
 
125
136
  async start() {
126
- await this.worker.start();
137
+ try {
138
+ this.worker = this.createWorker();
127
139
 
128
- this.worker.registerRunningInterceptor(this);
129
- this.markAsRunning(true);
140
+ await this.worker.start();
141
+ this.worker.registerRunningInterceptor(this);
142
+
143
+ this.markAsRunning(true);
144
+ } catch (error) {
145
+ await this.stop();
146
+ throw error;
147
+ }
130
148
  }
131
149
 
132
150
  async stop() {
133
- this.markAsRunning(false);
134
- this.worker.unregisterRunningInterceptor(this);
151
+ this.worker?.unregisterRunningInterceptor(this);
135
152
 
136
- const wasLastRunningInterceptor = this.numberOfRunningInterceptors() === 0;
137
- if (wasLastRunningInterceptor) {
138
- await this.worker.stop();
153
+ // The number of interceptors will be 0 if the first client could not start due to an error.
154
+ const isLastRunningInterceptor = this.numberOfRunningInterceptors === 0 || this.numberOfRunningInterceptors === 1;
155
+
156
+ if (isLastRunningInterceptor) {
157
+ await this.worker?.stop();
158
+ this.deleteWorker();
139
159
  }
160
+
161
+ this.markAsRunning(false);
162
+ this.worker = undefined;
140
163
  }
141
164
 
142
165
  private markAsRunning(isRunning: boolean) {
143
- if (this.worker instanceof LocalHttpInterceptorWorker) {
166
+ if (this.workerOrThrow.type === 'local') {
144
167
  this.store.markLocalInterceptorAsRunning(this, isRunning);
145
168
  } else {
146
169
  this.store.markRemoteInterceptorAsRunning(this, isRunning, this.baseURL);
@@ -148,8 +171,12 @@ class HttpInterceptorClient<
148
171
  this.isRunning = isRunning;
149
172
  }
150
173
 
151
- private numberOfRunningInterceptors() {
152
- if (this.worker instanceof LocalHttpInterceptorWorker) {
174
+ get numberOfRunningInterceptors() {
175
+ if (!this.isRunning) {
176
+ return 0;
177
+ }
178
+
179
+ if (this.workerOrThrow.type === 'local') {
153
180
  return this.store.numberOfRunningLocalInterceptors;
154
181
  } else {
155
182
  return this.store.numberOfRunningRemoteInterceptors(this.baseURL);
@@ -222,7 +249,7 @@ class HttpInterceptorClient<
222
249
  const url = joinURL(this.baseURLAsString, handler.path);
223
250
  const urlRegex = createRegExpFromURL(url);
224
251
 
225
- const registrationResult = this.worker.use(this, handler.method, url, async (context) => {
252
+ const registrationResult = this.workerOrThrow.use(this, handler.method, url, async (context) => {
226
253
  const response = await this.handleInterceptedRequest(
227
254
  urlRegex,
228
255
  handler.method,
@@ -316,11 +343,9 @@ class HttpInterceptorClient<
316
343
  }
317
344
 
318
345
  clear(options: { onCommitSuccess?: () => void; onCommitError?: () => void } = {}) {
319
- if (!this.isRunning) {
320
- throw new NotRunningHttpInterceptorError();
321
- }
322
-
323
- const clearResults: PossiblePromise<AnyHttpRequestHandlerClient | void>[] = [];
346
+ const clearResults: PossiblePromise<AnyHttpRequestHandlerClient | void>[] = [
347
+ this.workerOrThrow.clearInterceptorHandlers(this),
348
+ ];
324
349
 
325
350
  for (const method of HTTP_METHODS) {
326
351
  const newClearResults = this.clearMethodHandlers(method);
@@ -333,9 +358,6 @@ class HttpInterceptorClient<
333
358
  handlersByPath.clear();
334
359
  }
335
360
 
336
- const clearResult = this.worker.clearInterceptorHandlers(this);
337
- clearResults.push(clearResult);
338
-
339
361
  if (options.onCommitSuccess) {
340
362
  void Promise.all(clearResults).then(options.onCommitSuccess, options.onCommitError);
341
363
  }
@@ -7,6 +7,10 @@ import {
7
7
  } from '../interceptorWorker/types/options';
8
8
  import { AnyHttpInterceptorClient } from './HttpInterceptorClient';
9
9
 
10
+ interface RemoteWorkerKeyOptions {
11
+ auth: RemoteHttpInterceptorWorkerOptions['auth'];
12
+ }
13
+
10
14
  class HttpInterceptorStore {
11
15
  private static _localWorker?: LocalHttpInterceptorWorker;
12
16
  private static runningLocalInterceptors = new Set<AnyHttpInterceptorClient>();
@@ -20,8 +24,16 @@ class HttpInterceptorStore {
20
24
  return this.class._localWorker;
21
25
  }
22
26
 
23
- remoteWorker(baseURL: URL) {
24
- return this.class.remoteWorkers.get(baseURL.origin);
27
+ private getRemoteWorkerKey(baseURL: URL, options: RemoteWorkerKeyOptions) {
28
+ if (!options.auth) {
29
+ return baseURL.origin;
30
+ }
31
+ return `${baseURL.origin}:${options.auth.token}`;
32
+ }
33
+
34
+ remoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
35
+ const remoteWorkerKey = this.getRemoteWorkerKey(baseURL, options);
36
+ return this.class.remoteWorkers.get(remoteWorkerKey);
25
37
  }
26
38
 
27
39
  get numberOfRunningLocalInterceptors() {
@@ -70,23 +82,27 @@ class HttpInterceptorStore {
70
82
  return createdWorker;
71
83
  }
72
84
 
85
+ deleteLocalWorker() {
86
+ this.class._localWorker = undefined;
87
+ }
88
+
73
89
  getOrCreateRemoteWorker(workerOptions: Omit<RemoteHttpInterceptorWorkerOptions, 'type'>) {
74
- const existingWorker = this.class.remoteWorkers.get(workerOptions.serverURL.origin);
90
+ const remoteWorkerKey = this.getRemoteWorkerKey(workerOptions.serverURL, { auth: workerOptions.auth });
91
+ const existingWorker = this.class.remoteWorkers.get(remoteWorkerKey);
92
+
75
93
  if (existingWorker) {
76
94
  return existingWorker;
77
95
  }
78
96
 
79
97
  const createdWorker = createHttpInterceptorWorker({ ...workerOptions, type: 'remote' });
80
- this.class.remoteWorkers.set(workerOptions.serverURL.origin, createdWorker);
98
+ this.class.remoteWorkers.set(remoteWorkerKey, createdWorker);
81
99
 
82
100
  return createdWorker;
83
101
  }
84
102
 
85
- clear() {
86
- this.class._localWorker = undefined;
87
- this.class.runningLocalInterceptors.clear();
88
- this.class.remoteWorkers.clear();
89
- this.class.runningRemoteInterceptors.clear();
103
+ deleteRemoteWorker(baseURL: URL, options: RemoteWorkerKeyOptions) {
104
+ const remoteWorkerKey = this.getRemoteWorkerKey(baseURL, options);
105
+ this.class.remoteWorkers.delete(remoteWorkerKey);
90
106
  }
91
107
  }
92
108
 
@@ -15,12 +15,15 @@ class LocalHttpInterceptor<Schema extends HttpSchema> implements PublicLocalHttp
15
15
  constructor(options: LocalHttpInterceptorOptions) {
16
16
  const baseURL = new URL(options.baseURL);
17
17
 
18
- const worker = this.store.getOrCreateLocalWorker({});
19
-
20
18
  this.client = new HttpInterceptorClient<Schema, typeof LocalHttpRequestHandler>({
21
- worker,
22
19
  store: this.store,
23
20
  baseURL,
21
+ createWorker() {
22
+ return this.store.getOrCreateLocalWorker({});
23
+ },
24
+ deleteWorker() {
25
+ this.store.deleteLocalWorker();
26
+ },
24
27
  Handler: LocalHttpRequestHandler,
25
28
  onUnhandledRequest: options.onUnhandledRequest,
26
29
  requestSaving: options.requestSaving,
@@ -15,13 +15,18 @@ class RemoteHttpInterceptor<Schema extends HttpSchema> implements PublicRemoteHt
15
15
  constructor(options: RemoteHttpInterceptorOptions) {
16
16
  const baseURL = new URL(options.baseURL);
17
17
 
18
- const serverURL = new URL(baseURL.origin);
19
- const worker = this.store.getOrCreateRemoteWorker({ serverURL });
20
-
21
18
  this.client = new HttpInterceptorClient<Schema, typeof RemoteHttpRequestHandler>({
22
- worker,
23
19
  store: this.store,
24
20
  baseURL,
21
+ createWorker() {
22
+ return this.store.getOrCreateRemoteWorker({
23
+ serverURL: new URL(baseURL.origin),
24
+ auth: options.auth,
25
+ });
26
+ },
27
+ deleteWorker() {
28
+ this.store.deleteRemoteWorker(baseURL, { auth: options.auth });
29
+ },
25
30
  Handler: RemoteHttpRequestHandler,
26
31
  onUnhandledRequest: options.onUnhandledRequest,
27
32
  requestSaving: options.requestSaving,
@@ -160,6 +160,21 @@ export interface LocalHttpInterceptorOptions extends SharedHttpInterceptorOption
160
160
  export interface RemoteHttpInterceptorOptions extends SharedHttpInterceptorOptions {
161
161
  type: 'remote';
162
162
 
163
+ /**
164
+ * Options to authenticate the interceptor when connecting to an interceptor server. This is required if the
165
+ * interceptor server was started with the `--tokens-dir` option.
166
+ *
167
+ * @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
168
+ */
169
+ auth?: {
170
+ /**
171
+ * The authentication token to use.
172
+ *
173
+ * @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
174
+ */
175
+ token: string;
176
+ };
177
+
163
178
  /**
164
179
  * The strategy to use for unhandled requests. If a request starts with the base URL of the interceptor, but no
165
180
  * matching handler exists, this strategy will be used. If a function is provided, it will be called with the
@@ -16,9 +16,9 @@ import { Default, PossiblePromise } from '@zimic/utils/types';
16
16
  import color from 'picocolors';
17
17
 
18
18
  import { removeArrayElement } from '@/utils/arrays';
19
- import { formatValueToLog, logWithPrefix } from '@/utils/console';
20
19
  import { isClientSide } from '@/utils/environment';
21
20
  import { methodCanHaveResponseBody } from '@/utils/http';
21
+ import { formatValueToLog, logger } from '@/utils/logging';
22
22
 
23
23
  import HttpInterceptorClient, { AnyHttpInterceptorClient } from '../interceptor/HttpInterceptorClient';
24
24
  import { HttpInterceptorPlatform, HttpInterceptorType, UnhandledRequestStrategy } from '../interceptor/types/options';
@@ -35,10 +35,11 @@ import {
35
35
  import { DEFAULT_UNHANDLED_REQUEST_STRATEGY } from './constants';
36
36
  import InvalidFormDataError from './errors/InvalidFormDataError';
37
37
  import InvalidJSONError from './errors/InvalidJSONError';
38
+ import { HttpInterceptorWorkerType } from './types/options';
38
39
  import { HttpResponseFactory } from './types/requests';
39
40
 
40
41
  abstract class HttpInterceptorWorker {
41
- abstract readonly type: 'local' | 'remote';
42
+ abstract get type(): HttpInterceptorWorkerType;
42
43
 
43
44
  platform: HttpInterceptorPlatform | null = null;
44
45
  isRunning = false;
@@ -474,20 +475,17 @@ abstract class HttpInterceptorWorker {
474
475
  formatValueToLog(request.body),
475
476
  ]);
476
477
 
477
- logWithPrefix(
478
- [
479
- `${action === 'bypass' ? 'Warning:' : 'Error:'} Request was not handled and was ` +
480
- `${action === 'bypass' ? color.yellow('bypassed') : color.red('rejected')}.\n\n `,
481
- `${request.method} ${request.url}`,
482
- '\n Headers:',
483
- formattedHeaders,
484
- '\n Search params:',
485
- formattedSearchParams,
486
- '\n Body:',
487
- formattedBody,
488
- '\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api‐zimic‐interceptor‐http#unhandled-requests',
489
- ],
490
- { method: action === 'bypass' ? 'warn' : 'error' },
478
+ logger[action === 'bypass' ? 'warn' : 'error'](
479
+ `${action === 'bypass' ? 'Warning:' : 'Error:'} Request was not handled and was ` +
480
+ `${action === 'bypass' ? color.yellow('bypassed') : color.red('rejected')}.\n\n `,
481
+ `${request.method} ${request.url}`,
482
+ '\n Headers:',
483
+ formattedHeaders,
484
+ '\n Search params:',
485
+ formattedSearchParams,
486
+ '\n Body:',
487
+ formattedBody,
488
+ '\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api‐zimic‐interceptor‐http#unhandled-requests',
491
489
  );
492
490
  }
493
491
  }
@@ -6,7 +6,7 @@ import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from '@/server/ty
6
6
  import { importCrypto } from '@/utils/crypto';
7
7
  import { isClientSide, isServerSide } from '@/utils/environment';
8
8
  import { deserializeRequest, serializeResponse } from '@/utils/fetch';
9
- import { WebSocket } from '@/webSocket/types';
9
+ import { WebSocketEventMessage } from '@/webSocket/types';
10
10
  import WebSocketClient from '@/webSocket/WebSocketClient';
11
11
 
12
12
  import NotRunningHttpInterceptorError from '../interceptor/errors/NotRunningHttpInterceptorError';
@@ -26,24 +26,26 @@ interface HttpHandler {
26
26
  }
27
27
 
28
28
  class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
29
- webSocketClient: WebSocketClient<InterceptorServerWebSocketSchema>;
30
-
31
29
  private httpHandlers = new Map<HttpHandler['id'], HttpHandler>();
32
30
 
31
+ webSocketClient: WebSocketClient<InterceptorServerWebSocketSchema>;
32
+ private auth?: RemoteHttpInterceptorWorkerOptions['auth'];
33
+
33
34
  constructor(options: RemoteHttpInterceptorWorkerOptions) {
34
35
  super();
35
36
 
36
- const webSocketServerURL = this.deriveWebSocketServerURL(options.serverURL);
37
37
  this.webSocketClient = new WebSocketClient({
38
- url: webSocketServerURL.toString(),
38
+ url: this.getWebSocketServerURL(options.serverURL).toString(),
39
39
  });
40
+
41
+ this.auth = options.auth;
40
42
  }
41
43
 
42
44
  get type() {
43
45
  return 'remote' as const;
44
46
  }
45
47
 
46
- private deriveWebSocketServerURL(serverURL: URL) {
48
+ private getWebSocketServerURL(serverURL: URL) {
47
49
  const webSocketServerURL = new URL(serverURL);
48
50
  webSocketServerURL.protocol = serverURL.protocol.replace(/^http(s)?:$/, 'ws$1:');
49
51
  return webSocketServerURL;
@@ -51,7 +53,10 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
51
53
 
52
54
  async start() {
53
55
  await super.sharedStart(async () => {
54
- await this.webSocketClient.start();
56
+ await this.webSocketClient.start({
57
+ parameters: this.auth ? { token: this.auth.token } : undefined,
58
+ waitForAuthentication: true,
59
+ });
55
60
 
56
61
  this.webSocketClient.onEvent('interceptors/responses/create', this.createResponse);
57
62
  this.webSocketClient.onEvent('interceptors/responses/unhandled', this.handleUnhandledServerRequest);
@@ -62,7 +67,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
62
67
  }
63
68
 
64
69
  private createResponse = async (
65
- message: WebSocket.ServiceEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/create'>,
70
+ message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/create'>,
66
71
  ) => {
67
72
  const { handlerId, request: serializedRequest } = message.data;
68
73
 
@@ -87,7 +92,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
87
92
  };
88
93
 
89
94
  private handleUnhandledServerRequest = async (
90
- message: WebSocket.ServiceEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/unhandled'>,
95
+ message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/responses/unhandled'>,
91
96
  ) => {
92
97
  const { request: serializedRequest } = message.data;
93
98
  const request = deserializeRequest(serializedRequest);
@@ -156,7 +161,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
156
161
 
157
162
  this.httpHandlers.set(handler.id, handler);
158
163
 
159
- await this.webSocketClient.request('interceptors/workers/use/commit', {
164
+ await this.webSocketClient.request('interceptors/workers/commit', {
160
165
  id: handler.id,
161
166
  url: handler.url,
162
167
  method,
@@ -171,7 +176,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
171
176
  this.httpHandlers.clear();
172
177
 
173
178
  if (this.webSocketClient.isRunning) {
174
- await this.webSocketClient.request('interceptors/workers/use/reset', undefined);
179
+ await this.webSocketClient.request('interceptors/workers/reset', undefined);
175
180
  }
176
181
  }
177
182
 
@@ -193,7 +198,7 @@ class RemoteHttpInterceptorWorker extends HttpInterceptorWorker {
193
198
  method: handler.method,
194
199
  }));
195
200
 
196
- await this.webSocketClient.request('interceptors/workers/use/reset', groupsToRecommit);
201
+ await this.webSocketClient.request('interceptors/workers/reset', groupsToRecommit);
197
202
  }
198
203
  }
199
204
 
@@ -5,6 +5,7 @@ export interface LocalHttpInterceptorWorkerOptions {
5
5
  export interface RemoteHttpInterceptorWorkerOptions {
6
6
  type: 'remote';
7
7
  serverURL: URL;
8
+ auth?: { token: string };
8
9
  }
9
10
 
10
11
  export type HttpInterceptorWorkerOptions = LocalHttpInterceptorWorkerOptions | RemoteHttpInterceptorWorkerOptions;
@@ -3,7 +3,7 @@ import { Range } from '@zimic/utils/types';
3
3
  import color from 'picocolors';
4
4
 
5
5
  import { HttpInterceptorRequestSaving } from '@/http/interceptor/types/public';
6
- import { stringifyValueToLog } from '@/utils/console';
6
+ import { stringifyValueToLog } from '@/utils/logging';
7
7
 
8
8
  import { UnmatchedHttpInterceptorRequestGroup } from '../types/restrictions';
9
9
  import TimesDeclarationPointer from './TimesDeclarationPointer';
@@ -10,8 +10,8 @@ import { removeArrayIndex } from '@/utils/arrays';
10
10
  import { deserializeResponse, SerializedHttpRequest, serializeRequest } from '@/utils/fetch';
11
11
  import { getHttpServerPort, startHttpServer, stopHttpServer } from '@/utils/http';
12
12
  import { WebSocketMessageAbortError } from '@/utils/webSocket';
13
- import { WebSocket } from '@/webSocket/types';
14
- import WebSocketServer from '@/webSocket/WebSocketServer';
13
+ import { WebSocketEventMessage } from '@/webSocket/types';
14
+ import WebSocketServer, { WebSocketServerAuthenticate } from '@/webSocket/WebSocketServer';
15
15
 
16
16
  import {
17
17
  DEFAULT_ACCESS_CONTROL_HEADERS,
@@ -24,6 +24,7 @@ import RunningInterceptorServerError from './errors/RunningInterceptorServerErro
24
24
  import { InterceptorServerOptions } from './types/options';
25
25
  import { InterceptorServer as PublicInterceptorServer } from './types/public';
26
26
  import { HttpHandlerCommit, InterceptorServerWebSocketSchema } from './types/schema';
27
+ import { validateInterceptorToken } from './utils/auth';
27
28
  import { getFetchAPI } from './utils/fetch';
28
29
 
29
30
  interface HttpHandler {
@@ -42,6 +43,7 @@ class InterceptorServer implements PublicInterceptorServer {
42
43
  _hostname: string;
43
44
  _port: number | undefined;
44
45
  logUnhandledRequests: boolean;
46
+ tokensDirectory?: string;
45
47
 
46
48
  private httpHandlerGroups: {
47
49
  [Method in HttpMethod]: HttpHandler[];
@@ -61,6 +63,7 @@ class InterceptorServer implements PublicInterceptorServer {
61
63
  this._hostname = options.hostname ?? DEFAULT_HOSTNAME;
62
64
  this._port = options.port;
63
65
  this.logUnhandledRequests = options.logUnhandledRequests ?? DEFAULT_LOG_UNHANDLED_REQUESTS;
66
+ this.tokensDirectory = options.tokensDirectory;
64
67
  }
65
68
 
66
69
  get hostname() {
@@ -120,10 +123,48 @@ class InterceptorServer implements PublicInterceptorServer {
120
123
 
121
124
  this.webSocketServer = new WebSocketServer({
122
125
  httpServer: this.httpServer,
126
+ authenticate: this.authenticateWebSocketConnection,
123
127
  });
128
+
124
129
  this.startWebSocketServer();
125
130
  }
126
131
 
132
+ private authenticateWebSocketConnection: WebSocketServerAuthenticate = async (_socket, request) => {
133
+ if (!this.tokensDirectory) {
134
+ return { isValid: true };
135
+ }
136
+
137
+ const tokenValue = this.getWebSocketRequestTokenValue(request);
138
+
139
+ if (!tokenValue) {
140
+ return { isValid: false, message: 'An interceptor token is required, but none was provided.' };
141
+ }
142
+
143
+ try {
144
+ await validateInterceptorToken(tokenValue, { tokensDirectory: this.tokensDirectory });
145
+ return { isValid: true };
146
+ } catch (error) {
147
+ console.error(error);
148
+ return { isValid: false, message: 'The interceptor token is not valid.' };
149
+ }
150
+ };
151
+
152
+ private getWebSocketRequestTokenValue(request: IncomingMessage) {
153
+ const protocols = request.headers['sec-websocket-protocol'] ?? '';
154
+ const parametersAsString = decodeURIComponent(protocols).split(', ');
155
+
156
+ for (const parameterAsString of parametersAsString) {
157
+ const tokenValueMatch = /^token=(?<tokenValue>.+?)$/.exec(parameterAsString);
158
+ const tokenValue = tokenValueMatch?.groups?.tokenValue;
159
+
160
+ if (tokenValue) {
161
+ return tokenValue;
162
+ }
163
+ }
164
+
165
+ return undefined;
166
+ }
167
+
127
168
  private async startHttpServer() {
128
169
  await startHttpServer(this.httpServerOrThrow, {
129
170
  hostname: this.hostname,
@@ -136,22 +177,25 @@ class InterceptorServer implements PublicInterceptorServer {
136
177
 
137
178
  private startWebSocketServer() {
138
179
  this.webSocketServerOrThrow.start();
139
- this.webSocketServerOrThrow.onEvent('interceptors/workers/use/commit', this.commitWorker);
140
- this.webSocketServerOrThrow.onEvent('interceptors/workers/use/reset', this.resetWorker);
180
+
181
+ this.webSocketServerOrThrow.onEvent('interceptors/workers/commit', this.commitWorker);
182
+ this.webSocketServerOrThrow.onEvent('interceptors/workers/reset', this.resetWorker);
141
183
  }
142
184
 
143
185
  private commitWorker = (
144
- message: WebSocket.ServiceEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/use/commit'>,
186
+ message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/commit'>,
145
187
  socket: Socket,
146
188
  ) => {
147
189
  const commit = message.data;
190
+
148
191
  this.registerHttpHandler(commit, socket);
149
192
  this.registerWorkerSocketIfUnknown(socket);
193
+
150
194
  return {};
151
195
  };
152
196
 
153
197
  private resetWorker = (
154
- message: WebSocket.ServiceEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/use/reset'>,
198
+ message: WebSocketEventMessage<InterceptorServerWebSocketSchema, 'interceptors/workers/reset'>,
155
199
  socket: Socket,
156
200
  ) => {
157
201
  this.removeHttpHandlersBySocket(socket);
@@ -226,8 +270,8 @@ class InterceptorServer implements PublicInterceptorServer {
226
270
  }
227
271
 
228
272
  private async stopWebSocketServer() {
229
- this.webSocketServerOrThrow.offEvent('interceptors/workers/use/commit', this.commitWorker);
230
- this.webSocketServerOrThrow.offEvent('interceptors/workers/use/reset', this.resetWorker);
273
+ this.webSocketServerOrThrow.offEvent('interceptors/workers/commit', this.commitWorker);
274
+ this.webSocketServerOrThrow.offEvent('interceptors/workers/reset', this.resetWorker);
231
275
 
232
276
  await this.webSocketServerOrThrow.stop();
233
277
 
@@ -22,7 +22,7 @@ export const DEFAULT_ACCESS_CONTROL_HEADERS = Object.freeze({
22
22
  'access-control-allow-methods': ALLOWED_ACCESS_CONTROL_HTTP_METHODS,
23
23
  'access-control-allow-headers': '*',
24
24
  'access-control-expose-headers': '*',
25
- 'access-control-max-age': process.env.SERVER_ACCESS_CONTROL_MAX_AGE,
25
+ 'access-control-max-age': process.env.INTERCEPTOR_SERVER_ACCESS_CONTROL_MAX_AGE,
26
26
  }) satisfies AccessControlHeaders;
27
27
 
28
28
  /** The default status code for the preflight request. */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Error thrown when an interceptor token is invalid.
3
+ *
4
+ * @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
5
+ */
6
+ class InvalidInterceptorTokenError extends Error {
7
+ constructor(tokenId: string) {
8
+ super(`Invalid interceptor token: ${tokenId}`);
9
+ this.name = 'InvalidInterceptorTokenError';
10
+ }
11
+ }
12
+
13
+ export default InvalidInterceptorTokenError;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Error thrown when an interceptor token file is invalid.
3
+ *
4
+ * @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
5
+ */
6
+ class InvalidInterceptorTokenFileError extends Error {
7
+ constructor(tokenFilePath: string, validationErrorMessage: string) {
8
+ super(`Invalid interceptor token file ${tokenFilePath}: ${validationErrorMessage}`);
9
+ this.name = 'InvalidInterceptorTokenFileError';
10
+ }
11
+ }
12
+
13
+ export default InvalidInterceptorTokenFileError;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Error thrown when an interceptor token value is invalid.
3
+ *
4
+ * @see {@link https://github.com/zimicjs/zimic/wiki/cli‐zimic‐interceptor‐server#authentication Interceptor server authentication}
5
+ */
6
+ class InvalidInterceptorTokenValueError extends Error {
7
+ constructor(tokenValue: string) {
8
+ super(`Invalid interceptor token value: ${tokenValue}`);
9
+ this.name = 'InvalidInterceptorTokenValueError';
10
+ }
11
+ }
12
+
13
+ export default InvalidInterceptorTokenValueError;
@@ -21,4 +21,13 @@ export interface InterceptorServerOptions {
21
21
  * @default true
22
22
  */
23
23
  logUnhandledRequests?: boolean;
24
+
25
+ /**
26
+ * The directory where the authorized interceptor authentication tokens are saved. If provided, only remote
27
+ * interceptors bearing a valid token will be accepted. This option is essential if you are exposing your interceptor
28
+ * server publicly. For local development and testing, though, `--tokens-dir` is optional.
29
+ *
30
+ * @default undefined
31
+ */
32
+ tokensDirectory?: string;
24
33
  }