@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
@@ -2,8 +2,14 @@ import { __name } from './chunk-CGILA3WO.mjs';
2
2
  import { HTTP_METHODS, HttpHeaders, HttpSearchParams, HttpFormData, HTTP_METHODS_WITH_RESPONSE_BODY } from '@zimic/http';
3
3
  import { normalizeNodeRequest, sendNodeResponse } from '@whatwg-node/server';
4
4
  import { createServer } from 'http';
5
- import color2 from 'picocolors';
5
+ import color3 from 'picocolors';
6
6
  import ClientSocket from 'isomorphic-ws';
7
+ import crypto from 'crypto';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ import util from 'util';
12
+ import { z } from 'zod';
7
13
 
8
14
  // src/server/errors/RunningInterceptorServerError.ts
9
15
  var RunningInterceptorServerError = class extends Error {
@@ -112,6 +118,19 @@ function methodCanHaveResponseBody(method) {
112
118
  }
113
119
  __name(methodCanHaveResponseBody, "methodCanHaveResponseBody");
114
120
 
121
+ // src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts
122
+ var UnauthorizedWebSocketConnectionError = class extends Error {
123
+ constructor(event) {
124
+ super(`${event.reason} (code ${event.code})`);
125
+ this.event = event;
126
+ this.name = "UnauthorizedWebSocketConnectionError";
127
+ }
128
+ static {
129
+ __name(this, "UnauthorizedWebSocketConnectionError");
130
+ }
131
+ };
132
+ var UnauthorizedWebSocketConnectionError_default = UnauthorizedWebSocketConnectionError;
133
+
115
134
  // src/utils/webSocket.ts
116
135
  var WebSocketTimeoutError = class extends Error {
117
136
  static {
@@ -157,29 +176,58 @@ var WebSocketCloseTimeoutError = class extends WebSocketTimeoutError {
157
176
  var DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT = 60 * 1e3;
158
177
  var DEFAULT_WEB_SOCKET_MESSAGE_TIMEOUT = 3 * 60 * 1e3;
159
178
  async function waitForOpenClientSocket(socket, options = {}) {
160
- const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT } = options;
179
+ const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT, waitForAuthentication = false } = options;
161
180
  const isAlreadyOpen = socket.readyState === socket.OPEN;
162
181
  if (isAlreadyOpen) {
163
182
  return;
164
183
  }
165
184
  await new Promise((resolve, reject) => {
166
- function handleOpenError(error) {
185
+ function removeAllSocketListeners() {
186
+ socket.removeEventListener("message", handleSocketMessage);
167
187
  socket.removeEventListener("open", handleOpenSuccess);
188
+ socket.removeEventListener("error", handleOpenError);
189
+ socket.removeEventListener("close", handleClose);
190
+ }
191
+ __name(removeAllSocketListeners, "removeAllSocketListeners");
192
+ function handleOpenError(error) {
193
+ removeAllSocketListeners();
168
194
  reject(error);
169
195
  }
170
196
  __name(handleOpenError, "handleOpenError");
197
+ function handleClose(event) {
198
+ const isUnauthorized = event.code === 1008;
199
+ if (isUnauthorized) {
200
+ const unauthorizedError = new UnauthorizedWebSocketConnectionError_default(event);
201
+ handleOpenError(unauthorizedError);
202
+ } else {
203
+ handleOpenError(event);
204
+ }
205
+ }
206
+ __name(handleClose, "handleClose");
171
207
  const openTimeout = setTimeout(() => {
172
208
  const timeoutError = new WebSocketOpenTimeoutError(timeoutDuration);
173
209
  handleOpenError(timeoutError);
174
210
  }, timeoutDuration);
175
211
  function handleOpenSuccess() {
176
- socket.removeEventListener("error", handleOpenError);
212
+ removeAllSocketListeners();
177
213
  clearTimeout(openTimeout);
178
214
  resolve();
179
215
  }
180
216
  __name(handleOpenSuccess, "handleOpenSuccess");
181
- socket.addEventListener("open", handleOpenSuccess);
217
+ function handleSocketMessage(message) {
218
+ const hasValidAuth = message.data === "socket:auth:valid";
219
+ if (hasValidAuth) {
220
+ handleOpenSuccess();
221
+ }
222
+ }
223
+ __name(handleSocketMessage, "handleSocketMessage");
224
+ if (waitForAuthentication) {
225
+ socket.addEventListener("message", handleSocketMessage);
226
+ } else {
227
+ socket.addEventListener("open", handleOpenSuccess);
228
+ }
182
229
  socket.addEventListener("error", handleOpenError);
230
+ socket.addEventListener("close", handleClose);
183
231
  });
184
232
  }
185
233
  __name(waitForOpenClientSocket, "waitForOpenClientSocket");
@@ -190,23 +238,28 @@ async function closeClientSocket(socket, options = {}) {
190
238
  return;
191
239
  }
192
240
  await new Promise((resolve, reject) => {
193
- function handleCloseError(error) {
194
- socket.removeEventListener("close", handleCloseSuccess);
241
+ function removeAllSocketListeners() {
242
+ socket.removeEventListener("error", handleError);
243
+ socket.removeEventListener("close", handleClose);
244
+ }
245
+ __name(removeAllSocketListeners, "removeAllSocketListeners");
246
+ function handleError(error) {
247
+ removeAllSocketListeners();
195
248
  reject(error);
196
249
  }
197
- __name(handleCloseError, "handleCloseError");
250
+ __name(handleError, "handleError");
198
251
  const closeTimeout = setTimeout(() => {
199
252
  const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration);
200
- handleCloseError(timeoutError);
253
+ handleError(timeoutError);
201
254
  }, timeoutDuration);
202
- function handleCloseSuccess() {
203
- socket.removeEventListener("error", handleCloseError);
255
+ function handleClose() {
256
+ removeAllSocketListeners();
204
257
  clearTimeout(closeTimeout);
205
258
  resolve();
206
259
  }
207
- __name(handleCloseSuccess, "handleCloseSuccess");
208
- socket.addEventListener("error", handleCloseError);
209
- socket.addEventListener("close", handleCloseSuccess);
260
+ __name(handleClose, "handleClose");
261
+ socket.addEventListener("error", handleError);
262
+ socket.addEventListener("close", handleClose);
210
263
  socket.close();
211
264
  });
212
265
  }
@@ -295,6 +348,12 @@ function removeArrayElement(array, element) {
295
348
  }
296
349
  __name(removeArrayElement, "removeArrayElement");
297
350
 
351
+ // src/utils/environment.ts
352
+ function isClientSide() {
353
+ return typeof window !== "undefined" && typeof document !== "undefined";
354
+ }
355
+ __name(isClientSide, "isClientSide");
356
+
298
357
  // ../zimic-utils/dist/import/createCachedDynamicImport.mjs
299
358
  function createCachedDynamicImport(importModuleDynamically) {
300
359
  let cachedImportResult;
@@ -307,11 +366,87 @@ __name(createCachedDynamicImport, "createCachedDynamicImport");
307
366
  __name2(createCachedDynamicImport, "createCachedDynamicImport");
308
367
  var createCachedDynamicImport_default = createCachedDynamicImport;
309
368
 
310
- // src/utils/environment.ts
311
- function isClientSide() {
312
- return typeof window !== "undefined" && typeof document !== "undefined";
313
- }
314
- __name(isClientSide, "isClientSide");
369
+ // ../zimic-utils/dist/logging/Logger.mjs
370
+ var Logger = class _Logger {
371
+ static {
372
+ __name(this, "_Logger");
373
+ }
374
+ static {
375
+ __name2(this, "Logger");
376
+ }
377
+ prefix;
378
+ raw;
379
+ constructor(options = {}) {
380
+ const { prefix } = options;
381
+ this.prefix = prefix;
382
+ this.raw = prefix ? new _Logger({ ...options, prefix: void 0 }) : this;
383
+ }
384
+ logWithLevel(level, ...messages) {
385
+ if (this.prefix) {
386
+ console[level](this.prefix, ...messages);
387
+ } else {
388
+ console[level](...messages);
389
+ }
390
+ }
391
+ info(...messages) {
392
+ this.logWithLevel("log", ...messages);
393
+ }
394
+ warn(...messages) {
395
+ this.logWithLevel("warn", ...messages);
396
+ }
397
+ error(...messages) {
398
+ this.logWithLevel("error", ...messages);
399
+ }
400
+ table(headers, rows) {
401
+ const columnLengths = headers.map((header) => {
402
+ let maxValueLength = header.title.length;
403
+ for (const row of rows) {
404
+ const value = row[header.property];
405
+ if (value.length > maxValueLength) {
406
+ maxValueLength = value.length;
407
+ }
408
+ }
409
+ return maxValueLength;
410
+ });
411
+ const formattedRows = [];
412
+ const horizontalLine = columnLengths.map((length) => "\u2500".repeat(length));
413
+ formattedRows.push(horizontalLine, []);
414
+ for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) {
415
+ const header = headers[headerIndex];
416
+ const columnLength = columnLengths[headerIndex];
417
+ const value = header.title;
418
+ formattedRows.at(-1)?.push(value.padEnd(columnLength, " "));
419
+ }
420
+ formattedRows.push(horizontalLine);
421
+ for (const row of rows) {
422
+ formattedRows.push([]);
423
+ for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) {
424
+ const header = headers[headerIndex];
425
+ const columnLength = columnLengths[headerIndex];
426
+ const value = row[header.property];
427
+ formattedRows.at(-1)?.push(value.padEnd(columnLength, " "));
428
+ }
429
+ }
430
+ formattedRows.push(horizontalLine);
431
+ const formattedTable = formattedRows.map((row, index) => {
432
+ const isFirstLine = index === 0;
433
+ if (isFirstLine) {
434
+ return `\u250C\u2500${row.join("\u2500\u252C\u2500")}\u2500\u2510`;
435
+ }
436
+ const isLineAfterHeaders = index === 2;
437
+ if (isLineAfterHeaders) {
438
+ return `\u251C\u2500${row.join("\u2500\u253C\u2500")}\u2500\u2524`;
439
+ }
440
+ const isLastLine = index === formattedRows.length - 1;
441
+ if (isLastLine) {
442
+ return `\u2514\u2500${row.join("\u2500\u2534\u2500")}\u2500\u2518`;
443
+ }
444
+ return `\u2502 ${row.join(" \u2502 ")} \u2502`;
445
+ }).join("\n");
446
+ this.logWithLevel("log", formattedTable);
447
+ }
448
+ };
449
+ var Logger_default = Logger;
315
450
 
316
451
  // src/utils/files.ts
317
452
  var importFile = createCachedDynamicImport_default(
@@ -320,16 +455,30 @@ var importFile = createCachedDynamicImport_default(
320
455
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
321
456
  async () => globalThis.File ?? (await import('buffer')).File
322
457
  );
458
+ var importFilesystem = createCachedDynamicImport_default(() => import('fs'));
459
+ async function pathExists(path2) {
460
+ const fs2 = await importFilesystem();
461
+ try {
462
+ await fs2.promises.access(path2);
463
+ return true;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+ __name(pathExists, "pathExists");
323
469
 
324
- // src/utils/console.ts
470
+ // src/utils/logging.ts
471
+ var logger = new Logger_default({
472
+ prefix: color3.cyan("[@zimic/interceptor]")
473
+ });
325
474
  var importUtil = createCachedDynamicImport_default(() => import('util'));
326
475
  async function formatValueToLog(value, options = {}) {
327
476
  if (isClientSide()) {
328
477
  return value;
329
478
  }
330
479
  const { colors = true } = options;
331
- const util = await importUtil();
332
- return util.inspect(value, {
480
+ const util2 = await importUtil();
481
+ return util2.inspect(value, {
333
482
  colors,
334
483
  compact: true,
335
484
  depth: Infinity,
@@ -340,12 +489,6 @@ async function formatValueToLog(value, options = {}) {
340
489
  });
341
490
  }
342
491
  __name(formatValueToLog, "formatValueToLog");
343
- function logWithPrefix(messageOrMessages, options = {}) {
344
- const { method = "log" } = options;
345
- const messages = Array.isArray(messageOrMessages) ? messageOrMessages : [messageOrMessages];
346
- console[method](color2.cyan("[@zimic/interceptor]"), ...messages);
347
- }
348
- __name(logWithPrefix, "logWithPrefix");
349
492
 
350
493
  // src/http/requestHandler/types/requests.ts
351
494
  var HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES = Object.freeze(
@@ -705,21 +848,18 @@ var HttpInterceptorWorker = class _HttpInterceptorWorker {
705
848
  formatValueToLog(request.searchParams.toObject()),
706
849
  formatValueToLog(request.body)
707
850
  ]);
708
- logWithPrefix(
709
- [
710
- `${action === "bypass" ? "Warning:" : "Error:"} Request was not handled and was ${action === "bypass" ? color2.yellow("bypassed") : color2.red("rejected")}.
851
+ logger[action === "bypass" ? "warn" : "error"](
852
+ `${action === "bypass" ? "Warning:" : "Error:"} Request was not handled and was ${action === "bypass" ? color3.yellow("bypassed") : color3.red("rejected")}.
711
853
 
712
854
  `,
713
- `${request.method} ${request.url}`,
714
- "\n Headers:",
715
- formattedHeaders,
716
- "\n Search params:",
717
- formattedSearchParams,
718
- "\n Body:",
719
- formattedBody,
720
- "\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api\u2010zimic\u2010interceptor\u2010http#unhandled-requests"
721
- ],
722
- { method: action === "bypass" ? "warn" : "error" }
855
+ `${request.method} ${request.url}`,
856
+ "\n Headers:",
857
+ formattedHeaders,
858
+ "\n Search params:",
859
+ formattedSearchParams,
860
+ "\n Body:",
861
+ formattedBody,
862
+ "\n\nLearn more: https://github.com/zimicjs/zimic/wiki/api\u2010zimic\u2010interceptor\u2010http#unhandled-requests"
723
863
  );
724
864
  }
725
865
  };
@@ -751,6 +891,17 @@ function convertBase64ToArrayBuffer(base64Value) {
751
891
  }
752
892
  }
753
893
  __name(convertBase64ToArrayBuffer, "convertBase64ToArrayBuffer");
894
+ var HEX_REGEX = /^[a-z0-9]+$/;
895
+ function convertHexLengthToByteLength(hexLength) {
896
+ return Math.ceil(hexLength / 2);
897
+ }
898
+ __name(convertHexLengthToByteLength, "convertHexLengthToByteLength");
899
+ var BASE64URL_REGEX = /^[a-zA-Z0-9-_]+$/;
900
+ function convertHexLengthToBase64urlLength(hexLength) {
901
+ const byteLength = convertHexLengthToByteLength(hexLength);
902
+ return Math.ceil(byteLength * 4 / 3);
903
+ }
904
+ __name(convertHexLengthToBase64urlLength, "convertHexLengthToBase64urlLength");
754
905
 
755
906
  // src/utils/fetch.ts
756
907
  async function serializeRequest(request) {
@@ -788,6 +939,9 @@ var importCrypto = createCachedDynamicImport_default(async () => {
788
939
  return globalCrypto ?? await import('crypto');
789
940
  });
790
941
 
942
+ // src/webSocket/constants.ts
943
+ var WEB_SOCKET_CONTROL_MESSAGES = Object.freeze(["socket:auth:valid"]);
944
+
791
945
  // src/webSocket/errors/InvalidWebSocketMessage.ts
792
946
  var InvalidWebSocketMessage = class extends Error {
793
947
  static {
@@ -828,8 +982,11 @@ var WebSocketHandler = class {
828
982
  this.socketTimeout = options.socketTimeout ?? DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT;
829
983
  this.messageTimeout = options.messageTimeout ?? DEFAULT_WEB_SOCKET_MESSAGE_TIMEOUT;
830
984
  }
831
- async registerSocket(socket) {
832
- const openPromise = waitForOpenClientSocket(socket, { timeout: this.socketTimeout });
985
+ async registerSocket(socket, options = {}) {
986
+ const openPromise = waitForOpenClientSocket(socket, {
987
+ timeout: this.socketTimeout,
988
+ waitForAuthentication: options.waitForAuthentication
989
+ });
833
990
  const handleSocketMessage = /* @__PURE__ */ __name(async (rawMessage) => {
834
991
  await this.handleSocketMessage(socket, rawMessage);
835
992
  }, "handleSocketMessage");
@@ -842,8 +999,8 @@ var WebSocketHandler = class {
842
999
  socket.addEventListener("error", handleSocketError);
843
1000
  const handleSocketClose = /* @__PURE__ */ __name(() => {
844
1001
  socket.removeEventListener("message", handleSocketMessage);
845
- socket.removeEventListener("error", handleSocketError);
846
1002
  socket.removeEventListener("close", handleSocketClose);
1003
+ socket.removeEventListener("error", handleSocketError);
847
1004
  this.removeSocket(socket);
848
1005
  }, "handleSocketClose");
849
1006
  socket.addEventListener("close", handleSocketClose);
@@ -851,6 +1008,9 @@ var WebSocketHandler = class {
851
1008
  }
852
1009
  handleSocketMessage = /* @__PURE__ */ __name(async (socket, rawMessage) => {
853
1010
  try {
1011
+ if (this.isControlMessageData(rawMessage.data)) {
1012
+ return;
1013
+ }
854
1014
  const stringifiedMessageData = this.readRawMessageData(rawMessage.data);
855
1015
  const parsedMessageData = this.parseMessage(stringifiedMessageData);
856
1016
  await this.notifyListeners(parsedMessageData, socket);
@@ -858,6 +1018,9 @@ var WebSocketHandler = class {
858
1018
  console.error(error);
859
1019
  }
860
1020
  }, "handleSocketMessage");
1021
+ isControlMessageData(messageData) {
1022
+ return typeof messageData === "string" && WEB_SOCKET_CONTROL_MESSAGES.includes(messageData);
1023
+ }
861
1024
  readRawMessageData(data) {
862
1025
  if (typeof data === "string") {
863
1026
  return data;
@@ -872,7 +1035,7 @@ var WebSocketHandler = class {
872
1035
  } catch {
873
1036
  throw new InvalidWebSocketMessage_default(stringifiedMessage);
874
1037
  }
875
- if (!this.isValidMessage(parsedMessage)) {
1038
+ if (!this.isMessage(parsedMessage)) {
876
1039
  throw new InvalidWebSocketMessage_default(stringifiedMessage);
877
1040
  }
878
1041
  if (this.isReplyMessage(parsedMessage)) {
@@ -889,7 +1052,7 @@ var WebSocketHandler = class {
889
1052
  data: parsedMessage.data
890
1053
  };
891
1054
  }
892
- isValidMessage(message) {
1055
+ isMessage(message) {
893
1056
  return typeof message === "object" && message !== null && "id" in message && typeof message.id === "string" && "channel" in message && typeof message.channel === "string" && (!("requestId" in message) || typeof message.requestId === "string");
894
1057
  }
895
1058
  async notifyListeners(message, socket) {
@@ -925,9 +1088,9 @@ var WebSocketHandler = class {
925
1088
  this.sockets.delete(socket);
926
1089
  }
927
1090
  async createEventMessage(channel, eventData) {
928
- const crypto = await importCrypto();
1091
+ const crypto2 = await importCrypto();
929
1092
  const eventMessage = {
930
- id: crypto.randomUUID(),
1093
+ id: crypto2.randomUUID(),
931
1094
  channel,
932
1095
  data: eventData
933
1096
  };
@@ -977,9 +1140,9 @@ var WebSocketHandler = class {
977
1140
  }
978
1141
  }
979
1142
  async createReplyMessage(request, replyData) {
980
- const crypto = await importCrypto();
1143
+ const crypto2 = await importCrypto();
981
1144
  const replyMessage = {
982
- id: crypto.randomUUID(),
1145
+ id: crypto2.randomUUID(),
983
1146
  channel: request.channel,
984
1147
  requestId: request.id,
985
1148
  data: replyData
@@ -1060,12 +1223,14 @@ var WebSocketServer = class extends WebSocketHandler_default {
1060
1223
  }
1061
1224
  webSocketServer;
1062
1225
  httpServer;
1226
+ authenticate;
1063
1227
  constructor(options) {
1064
1228
  super({
1065
1229
  socketTimeout: options.socketTimeout,
1066
1230
  messageTimeout: options.messageTimeout
1067
1231
  });
1068
1232
  this.httpServer = options.httpServer;
1233
+ this.authenticate = options.authenticate;
1069
1234
  }
1070
1235
  get isRunning() {
1071
1236
  return this.webSocketServer !== void 0;
@@ -1078,9 +1243,17 @@ var WebSocketServer = class extends WebSocketHandler_default {
1078
1243
  webSocketServer.on("error", (error) => {
1079
1244
  console.error(error);
1080
1245
  });
1081
- webSocketServer.on("connection", async (socket) => {
1246
+ webSocketServer.on("connection", async (socket, request) => {
1247
+ if (this.authenticate) {
1248
+ const result = await this.authenticate(socket, request);
1249
+ if (!result.isValid) {
1250
+ socket.close(1008, result.message);
1251
+ return;
1252
+ }
1253
+ }
1082
1254
  try {
1083
1255
  await super.registerSocket(socket);
1256
+ socket.send("socket:auth:valid");
1084
1257
  } catch (error) {
1085
1258
  webSocketServer.emit("error", error);
1086
1259
  }
@@ -1101,8 +1274,242 @@ var WebSocketServer = class extends WebSocketHandler_default {
1101
1274
  };
1102
1275
  var WebSocketServer_default = WebSocketServer;
1103
1276
 
1277
+ // src/server/errors/InvalidInterceptorTokenError.ts
1278
+ var InvalidInterceptorTokenError = class extends Error {
1279
+ static {
1280
+ __name(this, "InvalidInterceptorTokenError");
1281
+ }
1282
+ constructor(tokenId) {
1283
+ super(`Invalid interceptor token: ${tokenId}`);
1284
+ this.name = "InvalidInterceptorTokenError";
1285
+ }
1286
+ };
1287
+ var InvalidInterceptorTokenError_default = InvalidInterceptorTokenError;
1288
+
1289
+ // src/server/errors/InvalidInterceptorTokenFileError.ts
1290
+ var InvalidInterceptorTokenFileError = class extends Error {
1291
+ static {
1292
+ __name(this, "InvalidInterceptorTokenFileError");
1293
+ }
1294
+ constructor(tokenFilePath, validationErrorMessage) {
1295
+ super(`Invalid interceptor token file ${tokenFilePath}: ${validationErrorMessage}`);
1296
+ this.name = "InvalidInterceptorTokenFileError";
1297
+ }
1298
+ };
1299
+ var InvalidInterceptorTokenFileError_default = InvalidInterceptorTokenFileError;
1300
+
1301
+ // src/server/errors/InvalidInterceptorTokenValueError.ts
1302
+ var InvalidInterceptorTokenValueError = class extends Error {
1303
+ static {
1304
+ __name(this, "InvalidInterceptorTokenValueError");
1305
+ }
1306
+ constructor(tokenValue) {
1307
+ super(`Invalid interceptor token value: ${tokenValue}`);
1308
+ this.name = "InvalidInterceptorTokenValueError";
1309
+ }
1310
+ };
1311
+ var InvalidInterceptorTokenValueError_default = InvalidInterceptorTokenValueError;
1312
+
1313
+ // src/server/utils/auth.ts
1314
+ var DEFAULT_INTERCEPTOR_TOKENS_DIRECTORY = path.join(
1315
+ ".zimic",
1316
+ "interceptor",
1317
+ "server",
1318
+ `tokens${""}`
1319
+ );
1320
+ var INTERCEPTOR_TOKEN_ID_HEX_LENGTH = 32;
1321
+ var INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH = 64;
1322
+ var INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH = INTERCEPTOR_TOKEN_ID_HEX_LENGTH + INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH;
1323
+ var INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH = convertHexLengthToBase64urlLength(
1324
+ INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH
1325
+ );
1326
+ var INTERCEPTOR_TOKEN_SALT_HEX_LENGTH = 64;
1327
+ var INTERCEPTOR_TOKEN_HASH_ITERATIONS = Number("1000000");
1328
+ var INTERCEPTOR_TOKEN_HASH_HEX_LENGTH = 128;
1329
+ var INTERCEPTOR_TOKEN_HASH_ALGORITHM = "sha512";
1330
+ var pbkdf2 = util.promisify(crypto.pbkdf2);
1331
+ async function hashInterceptorToken(plainToken, salt) {
1332
+ const hashBuffer = await pbkdf2(
1333
+ plainToken,
1334
+ salt,
1335
+ INTERCEPTOR_TOKEN_HASH_ITERATIONS,
1336
+ convertHexLengthToByteLength(INTERCEPTOR_TOKEN_HASH_HEX_LENGTH),
1337
+ INTERCEPTOR_TOKEN_HASH_ALGORITHM
1338
+ );
1339
+ const hash = hashBuffer.toString("hex");
1340
+ return hash;
1341
+ }
1342
+ __name(hashInterceptorToken, "hashInterceptorToken");
1343
+ function createInterceptorTokenId() {
1344
+ return crypto.randomUUID().replace(/[^a-z0-9]/g, "");
1345
+ }
1346
+ __name(createInterceptorTokenId, "createInterceptorTokenId");
1347
+ function isValidInterceptorTokenId(tokenId) {
1348
+ return tokenId.length === INTERCEPTOR_TOKEN_ID_HEX_LENGTH && HEX_REGEX.test(tokenId);
1349
+ }
1350
+ __name(isValidInterceptorTokenId, "isValidInterceptorTokenId");
1351
+ function isValidInterceptorTokenValue(tokenValue) {
1352
+ return tokenValue.length === INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH && BASE64URL_REGEX.test(tokenValue);
1353
+ }
1354
+ __name(isValidInterceptorTokenValue, "isValidInterceptorTokenValue");
1355
+ async function createInterceptorTokensDirectory(tokensDirectory) {
1356
+ try {
1357
+ const parentTokensDirectory = path.dirname(tokensDirectory);
1358
+ await fs.promises.mkdir(parentTokensDirectory, { recursive: true });
1359
+ await fs.promises.mkdir(tokensDirectory, { mode: 448, recursive: true });
1360
+ await fs.promises.appendFile(path.join(tokensDirectory, ".gitignore"), `*${os.EOL}`, { encoding: "utf-8" });
1361
+ } catch (error) {
1362
+ logger.error(
1363
+ `${color3.red(color3.bold("\u2716"))} Failed to create the tokens directory: ${color3.magenta(tokensDirectory)}`
1364
+ );
1365
+ throw error;
1366
+ }
1367
+ }
1368
+ __name(createInterceptorTokensDirectory, "createInterceptorTokensDirectory");
1369
+ var interceptorTokenFileContentSchema = z.object({
1370
+ version: z.literal(1),
1371
+ token: z.object({
1372
+ id: z.string().length(INTERCEPTOR_TOKEN_ID_HEX_LENGTH).regex(HEX_REGEX),
1373
+ name: z.string().optional(),
1374
+ secret: z.object({
1375
+ hash: z.string().length(INTERCEPTOR_TOKEN_HASH_HEX_LENGTH).regex(HEX_REGEX),
1376
+ salt: z.string().length(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH).regex(HEX_REGEX)
1377
+ }),
1378
+ createdAt: z.string().datetime().transform((value) => new Date(value))
1379
+ })
1380
+ });
1381
+ async function saveInterceptorTokenToFile(tokensDirectory, token) {
1382
+ const tokeFilePath = path.join(tokensDirectory, token.id);
1383
+ const persistedToken = {
1384
+ id: token.id,
1385
+ name: token.name,
1386
+ secret: {
1387
+ hash: token.secret.hash,
1388
+ salt: token.secret.salt
1389
+ },
1390
+ createdAt: token.createdAt.toISOString()
1391
+ };
1392
+ const tokenFileContent = interceptorTokenFileContentSchema.parse({
1393
+ version: 1,
1394
+ token: persistedToken
1395
+ });
1396
+ await fs.promises.writeFile(tokeFilePath, JSON.stringify(tokenFileContent, null, 2), {
1397
+ mode: 384,
1398
+ encoding: "utf-8"
1399
+ });
1400
+ return tokeFilePath;
1401
+ }
1402
+ __name(saveInterceptorTokenToFile, "saveInterceptorTokenToFile");
1403
+ async function readInterceptorTokenFromFile(tokenId, options) {
1404
+ if (!isValidInterceptorTokenId(tokenId)) {
1405
+ throw new InvalidInterceptorTokenError_default(tokenId);
1406
+ }
1407
+ const tokenFilePath = path.join(options.tokensDirectory, tokenId);
1408
+ const tokenFileExists = await pathExists(tokenFilePath);
1409
+ if (!tokenFileExists) {
1410
+ return null;
1411
+ }
1412
+ const tokenFileContentAsString = await fs.promises.readFile(tokenFilePath, { encoding: "utf-8" });
1413
+ const validation = interceptorTokenFileContentSchema.safeParse(JSON.parse(tokenFileContentAsString));
1414
+ if (!validation.success) {
1415
+ throw new InvalidInterceptorTokenFileError_default(tokenFilePath, validation.error.message);
1416
+ }
1417
+ return validation.data.token;
1418
+ }
1419
+ __name(readInterceptorTokenFromFile, "readInterceptorTokenFromFile");
1420
+ async function createInterceptorToken(options) {
1421
+ const { name, tokensDirectory } = options;
1422
+ const tokensDirectoryExists = await pathExists(tokensDirectory);
1423
+ if (!tokensDirectoryExists) {
1424
+ await createInterceptorTokensDirectory(tokensDirectory);
1425
+ }
1426
+ const tokenId = createInterceptorTokenId();
1427
+ if (!isValidInterceptorTokenId(tokenId)) {
1428
+ throw new InvalidInterceptorTokenError_default(tokenId);
1429
+ }
1430
+ const tokenSecretSizeInBytes = convertHexLengthToByteLength(INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH);
1431
+ const tokenSecret = crypto.randomBytes(tokenSecretSizeInBytes).toString("hex");
1432
+ const tokenSecretSaltSizeInBytes = convertHexLengthToByteLength(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH);
1433
+ const tokenSecretSalt = crypto.randomBytes(tokenSecretSaltSizeInBytes).toString("hex");
1434
+ const tokenSecretHash = await hashInterceptorToken(tokenSecret, tokenSecretSalt);
1435
+ const tokenValue = Buffer.from(`${tokenId}${tokenSecret}`, "hex").toString("base64url");
1436
+ if (!isValidInterceptorTokenValue(tokenValue)) {
1437
+ throw new InvalidInterceptorTokenValueError_default(tokenValue);
1438
+ }
1439
+ const token = {
1440
+ id: tokenId,
1441
+ name,
1442
+ secret: {
1443
+ hash: tokenSecretHash,
1444
+ salt: tokenSecretSalt,
1445
+ value: tokenSecret
1446
+ },
1447
+ value: tokenValue,
1448
+ createdAt: /* @__PURE__ */ new Date()
1449
+ };
1450
+ await saveInterceptorTokenToFile(tokensDirectory, token);
1451
+ return token;
1452
+ }
1453
+ __name(createInterceptorToken, "createInterceptorToken");
1454
+ async function listInterceptorTokens(options) {
1455
+ const tokensDirectoryExists = await pathExists(options.tokensDirectory);
1456
+ if (!tokensDirectoryExists) {
1457
+ return [];
1458
+ }
1459
+ const files = await fs.promises.readdir(options.tokensDirectory);
1460
+ const tokenReadPromises = files.map(async (file) => {
1461
+ if (!isValidInterceptorTokenId(file)) {
1462
+ return null;
1463
+ }
1464
+ const tokenId = file;
1465
+ const token = await readInterceptorTokenFromFile(tokenId, options);
1466
+ return token;
1467
+ });
1468
+ const tokenCandidates = await Promise.allSettled(tokenReadPromises);
1469
+ const tokens = [];
1470
+ for (const tokenCandidate of tokenCandidates) {
1471
+ if (tokenCandidate.status === "rejected") {
1472
+ console.error(tokenCandidate.reason);
1473
+ } else if (tokenCandidate.value !== null) {
1474
+ tokens.push(tokenCandidate.value);
1475
+ }
1476
+ }
1477
+ tokens.sort((token, otherToken) => token.createdAt.getTime() - otherToken.createdAt.getTime());
1478
+ return tokens;
1479
+ }
1480
+ __name(listInterceptorTokens, "listInterceptorTokens");
1481
+ async function validateInterceptorToken(tokenValue, options) {
1482
+ if (!isValidInterceptorTokenValue(tokenValue)) {
1483
+ throw new InvalidInterceptorTokenValueError_default(tokenValue);
1484
+ }
1485
+ const decodedTokenValue = Buffer.from(tokenValue, "base64url").toString("hex");
1486
+ const tokenId = decodedTokenValue.slice(0, INTERCEPTOR_TOKEN_ID_HEX_LENGTH);
1487
+ const tokenSecret = decodedTokenValue.slice(
1488
+ INTERCEPTOR_TOKEN_ID_HEX_LENGTH,
1489
+ INTERCEPTOR_TOKEN_ID_HEX_LENGTH + INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH
1490
+ );
1491
+ const tokenFromFile = await readInterceptorTokenFromFile(tokenId, options);
1492
+ if (!tokenFromFile) {
1493
+ throw new InvalidInterceptorTokenValueError_default(tokenValue);
1494
+ }
1495
+ const tokenSecretHash = await hashInterceptorToken(tokenSecret, tokenFromFile.secret.salt);
1496
+ if (tokenSecretHash !== tokenFromFile.secret.hash) {
1497
+ throw new InvalidInterceptorTokenValueError_default(tokenValue);
1498
+ }
1499
+ }
1500
+ __name(validateInterceptorToken, "validateInterceptorToken");
1501
+ async function removeInterceptorToken(tokenId, options) {
1502
+ if (!isValidInterceptorTokenId(tokenId)) {
1503
+ throw new InvalidInterceptorTokenError_default(tokenId);
1504
+ }
1505
+ const tokenFilePath = path.join(options.tokensDirectory, tokenId);
1506
+ await fs.promises.rm(tokenFilePath, { force: true });
1507
+ }
1508
+ __name(removeInterceptorToken, "removeInterceptorToken");
1509
+
1104
1510
  // src/server/utils/fetch.ts
1105
1511
  async function getFetchAPI() {
1512
+ const File2 = await importFile();
1106
1513
  return {
1107
1514
  fetch,
1108
1515
  Request,
@@ -1117,7 +1524,7 @@ async function getFetchAPI() {
1117
1524
  TextDecoderStream,
1118
1525
  TextEncoderStream,
1119
1526
  Blob,
1120
- File: await importFile(),
1527
+ File: File2,
1121
1528
  crypto: globalThis.crypto,
1122
1529
  btoa,
1123
1530
  TextEncoder,
@@ -1139,6 +1546,7 @@ var InterceptorServer = class {
1139
1546
  _hostname;
1140
1547
  _port;
1141
1548
  logUnhandledRequests;
1549
+ tokensDirectory;
1142
1550
  httpHandlerGroups = {
1143
1551
  GET: [],
1144
1552
  POST: [],
@@ -1153,6 +1561,7 @@ var InterceptorServer = class {
1153
1561
  this._hostname = options.hostname ?? DEFAULT_HOSTNAME;
1154
1562
  this._port = options.port;
1155
1563
  this.logUnhandledRequests = options.logUnhandledRequests ?? DEFAULT_LOG_UNHANDLED_REQUESTS;
1564
+ this.tokensDirectory = options.tokensDirectory;
1156
1565
  }
1157
1566
  get hostname() {
1158
1567
  return this._hostname;
@@ -1197,10 +1606,39 @@ var InterceptorServer = class {
1197
1606
  });
1198
1607
  await this.startHttpServer();
1199
1608
  this.webSocketServer = new WebSocketServer_default({
1200
- httpServer: this.httpServer
1609
+ httpServer: this.httpServer,
1610
+ authenticate: this.authenticateWebSocketConnection
1201
1611
  });
1202
1612
  this.startWebSocketServer();
1203
1613
  }
1614
+ authenticateWebSocketConnection = /* @__PURE__ */ __name(async (_socket, request) => {
1615
+ if (!this.tokensDirectory) {
1616
+ return { isValid: true };
1617
+ }
1618
+ const tokenValue = this.getWebSocketRequestTokenValue(request);
1619
+ if (!tokenValue) {
1620
+ return { isValid: false, message: "An interceptor token is required, but none was provided." };
1621
+ }
1622
+ try {
1623
+ await validateInterceptorToken(tokenValue, { tokensDirectory: this.tokensDirectory });
1624
+ return { isValid: true };
1625
+ } catch (error) {
1626
+ console.error(error);
1627
+ return { isValid: false, message: "The interceptor token is not valid." };
1628
+ }
1629
+ }, "authenticateWebSocketConnection");
1630
+ getWebSocketRequestTokenValue(request) {
1631
+ const protocols = request.headers["sec-websocket-protocol"] ?? "";
1632
+ const parametersAsString = decodeURIComponent(protocols).split(", ");
1633
+ for (const parameterAsString of parametersAsString) {
1634
+ const tokenValueMatch = /^token=(?<tokenValue>.+?)$/.exec(parameterAsString);
1635
+ const tokenValue = tokenValueMatch?.groups?.tokenValue;
1636
+ if (tokenValue) {
1637
+ return tokenValue;
1638
+ }
1639
+ }
1640
+ return void 0;
1641
+ }
1204
1642
  async startHttpServer() {
1205
1643
  await startHttpServer(this.httpServerOrThrow, {
1206
1644
  hostname: this.hostname,
@@ -1211,8 +1649,8 @@ var InterceptorServer = class {
1211
1649
  }
1212
1650
  startWebSocketServer() {
1213
1651
  this.webSocketServerOrThrow.start();
1214
- this.webSocketServerOrThrow.onEvent("interceptors/workers/use/commit", this.commitWorker);
1215
- this.webSocketServerOrThrow.onEvent("interceptors/workers/use/reset", this.resetWorker);
1652
+ this.webSocketServerOrThrow.onEvent("interceptors/workers/commit", this.commitWorker);
1653
+ this.webSocketServerOrThrow.onEvent("interceptors/workers/reset", this.resetWorker);
1216
1654
  }
1217
1655
  commitWorker = /* @__PURE__ */ __name((message, socket) => {
1218
1656
  const commit = message.data;
@@ -1276,8 +1714,8 @@ var InterceptorServer = class {
1276
1714
  this.httpServer = void 0;
1277
1715
  }
1278
1716
  async stopWebSocketServer() {
1279
- this.webSocketServerOrThrow.offEvent("interceptors/workers/use/commit", this.commitWorker);
1280
- this.webSocketServerOrThrow.offEvent("interceptors/workers/use/reset", this.resetWorker);
1717
+ this.webSocketServerOrThrow.offEvent("interceptors/workers/commit", this.commitWorker);
1718
+ this.webSocketServerOrThrow.offEvent("interceptors/workers/reset", this.resetWorker);
1281
1719
  await this.webSocketServerOrThrow.stop();
1282
1720
  this.webSocketServer = void 0;
1283
1721
  }
@@ -1389,6 +1827,10 @@ __name(createInterceptorServer, "createInterceptorServer");
1389
1827
  * This is expected not to happen since the servers are not stopped unless they are running. */
1390
1828
  /* istanbul ignore if -- @preserve
1391
1829
  * The address is expected to be an object because the server does not listen on a pipe or Unix domain socket. */
1830
+ /* istanbul ignore else -- @preserve
1831
+ * An unauthorized close event is the only one we expect to happen here. */
1832
+ /* istanbul ignore else -- @preserve
1833
+ * We currently only support the 'socket:auth:valid' message and it is the only possible control message here. */
1392
1834
  /* istanbul ignore if -- @preserve
1393
1835
  * This is not expected since the server is not stopped unless it is running. */
1394
1836
  /* istanbul ignore next -- @preserve
@@ -1400,6 +1842,12 @@ __name(createInterceptorServer, "createInterceptorServer");
1400
1842
  /* istanbul ignore next -- @preserve
1401
1843
  * Reply listeners are always present when notified in normal conditions. If they were not present, the request
1402
1844
  * would reach a timeout and not be responded. The empty set serves as a fallback. */
1845
+ /* istanbul ignore if -- @preserve
1846
+ * This should never happen, but let's check that the token identifier is valid after generated. */
1847
+ /* istanbul ignore if -- @preserve
1848
+ * This should never happen, but let's check that the token value is valid after generated. */
1849
+ /* istanbul ignore if -- @preserve
1850
+ * At this point, we should have a valid tokenId. This is just a sanity check. */
1403
1851
  /* istanbul ignore if -- @preserve
1404
1852
  * The HTTP server is initialized before using this method in normal conditions. */
1405
1853
  /* istanbul ignore if -- @preserve
@@ -1410,6 +1858,6 @@ __name(createInterceptorServer, "createInterceptorServer");
1410
1858
  * This try..catch is for the case when the remote interceptor web socket client is closed before responding.
1411
1859
  * Since simulating this scenario is difficult, we are ignoring this branch fow now. */
1412
1860
 
1413
- export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default, RunningInterceptorServerError_default, createCachedDynamicImport_default, createInterceptorServer, logWithPrefix };
1414
- //# sourceMappingURL=chunk-3SKHNQLL.mjs.map
1415
- //# sourceMappingURL=chunk-3SKHNQLL.mjs.map
1861
+ export { DEFAULT_ACCESS_CONTROL_HEADERS, DEFAULT_INTERCEPTOR_TOKENS_DIRECTORY, DEFAULT_PREFLIGHT_STATUS_CODE, NotRunningInterceptorServerError_default, RunningInterceptorServerError_default, createCachedDynamicImport_default, createInterceptorServer, createInterceptorToken, listInterceptorTokens, logger, readInterceptorTokenFromFile, removeInterceptorToken };
1862
+ //# sourceMappingURL=chunk-L75WKVZO.mjs.map
1863
+ //# sourceMappingURL=chunk-L75WKVZO.mjs.map