@zimic/interceptor
Version:
Next-gen TypeScript-first HTTP intercepting and mocking
1,410 lines (1,374 loc) • 65.3 kB
JavaScript
'use strict';
var http = require('@zimic/http');
var server = require('@whatwg-node/server');
var http$1 = require('http');
var color3 = require('picocolors');
var ClientSocket = require('isomorphic-ws');
var crypto = require('crypto');
var fs = require('fs');
var os = require('os');
var path = require('path');
var util = require('util');
var z = require('zod');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var color3__default = /*#__PURE__*/_interopDefault(color3);
var ClientSocket__default = /*#__PURE__*/_interopDefault(ClientSocket);
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
var fs__default = /*#__PURE__*/_interopDefault(fs);
var os__default = /*#__PURE__*/_interopDefault(os);
var path__default = /*#__PURE__*/_interopDefault(path);
var util__default = /*#__PURE__*/_interopDefault(util);
var z__namespace = /*#__PURE__*/_interopNamespace(z);
// src/server/errors/RunningInterceptorServerError.ts
var RunningInterceptorServerError = class extends Error {
constructor(additionalMessage) {
super(`The interceptor server is running.${additionalMessage}`);
this.name = "RunningInterceptorServerError";
}
};
var RunningInterceptorServerError_default = RunningInterceptorServerError;
// src/server/errors/NotRunningInterceptorServerError.ts
var NotRunningInterceptorServerError = class extends Error {
constructor() {
super("The interceptor server is not running. Did you forget to start it?");
this.name = "NotRunningInterceptorServerError";
}
};
var NotRunningInterceptorServerError_default = NotRunningInterceptorServerError;
// ../zimic-utils/dist/server/lifecycle.mjs
var DEFAULT_HTTP_SERVER_LIFECYCLE_TIMEOUT = 60 * 1e3;
var HttpServerTimeoutError = class extends Error {
constructor(message) {
super(message);
this.name = "HttpServerTimeoutError";
}
};
var HttpServerStartTimeoutError = class extends HttpServerTimeoutError {
constructor(reachedTimeout) {
super(`HTTP server start timed out after ${reachedTimeout}ms.`);
this.name = "HttpServerStartTimeoutError";
}
};
var HttpServerStopTimeoutError = class extends HttpServerTimeoutError {
constructor(reachedTimeout) {
super(`HTTP server stop timed out after ${reachedTimeout}ms.`);
this.name = "HttpServerStopTimeoutError";
}
};
async function startHttpServer(server, options = {}) {
const { hostname, port, timeout: timeoutDuration = DEFAULT_HTTP_SERVER_LIFECYCLE_TIMEOUT } = options;
await new Promise((resolve, reject) => {
function handleStartError(error) {
server.off("listening", handleStartSuccess);
reject(error);
}
const startTimeout = setTimeout(() => {
const timeoutError = new HttpServerStartTimeoutError(timeoutDuration);
handleStartError(timeoutError);
}, timeoutDuration);
function handleStartSuccess() {
server.off("error", handleStartError);
clearTimeout(startTimeout);
resolve();
}
server.once("error", handleStartError);
server.listen(port, hostname, handleStartSuccess);
});
}
async function stopHttpServer(server, options = {}) {
const { timeout: timeoutDuration = DEFAULT_HTTP_SERVER_LIFECYCLE_TIMEOUT } = options;
if (!server.listening) {
return;
}
await new Promise((resolve, reject) => {
const stopTimeout = setTimeout(() => {
const timeoutError = new HttpServerStopTimeoutError(timeoutDuration);
reject(timeoutError);
}, timeoutDuration);
server.close((error) => {
clearTimeout(stopTimeout);
if (error) {
reject(error);
} else {
resolve();
}
});
server.closeAllConnections();
});
}
function getHttpServerPort(server) {
const address = server.address();
if (typeof address === "string") {
return void 0;
} else {
return address?.port;
}
}
// src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts
var UnauthorizedWebSocketConnectionError = class extends Error {
constructor(event) {
super(`${event.reason} (code ${event.code})`);
this.event = event;
this.name = "UnauthorizedWebSocketConnectionError";
}
};
var UnauthorizedWebSocketConnectionError_default = UnauthorizedWebSocketConnectionError;
// src/utils/webSocket.ts
var WebSocketTimeoutError = class extends Error {
};
var WebSocketOpenTimeoutError = class extends WebSocketTimeoutError {
constructor(reachedTimeout) {
super(`Web socket open timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketOpenTimeoutError";
}
};
var WebSocketMessageTimeoutError = class extends WebSocketTimeoutError {
constructor(reachedTimeout) {
super(`Web socket message timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketMessageTimeoutError";
}
};
var WebSocketMessageAbortError = class extends WebSocketTimeoutError {
constructor() {
super("Web socket message was aborted.");
this.name = "WebSocketMessageAbortError";
}
};
var WebSocketCloseTimeoutError = class extends WebSocketTimeoutError {
constructor(reachedTimeout) {
super(`Web socket close timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketCloseTimeoutError";
}
};
var DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT = 60 * 1e3;
var DEFAULT_WEB_SOCKET_MESSAGE_TIMEOUT = 3 * 60 * 1e3;
async function waitForOpenClientSocket(socket, options = {}) {
const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT, waitForAuthentication = false } = options;
const isAlreadyOpen = socket.readyState === socket.OPEN;
if (isAlreadyOpen) {
return;
}
await new Promise((resolve, reject) => {
function removeAllSocketListeners() {
socket.removeEventListener("message", handleSocketMessage);
socket.removeEventListener("open", handleOpenSuccess);
socket.removeEventListener("error", handleOpenError);
socket.removeEventListener("close", handleClose);
}
function handleOpenError(error) {
removeAllSocketListeners();
reject(error);
}
function handleClose(event) {
const isUnauthorized = event.code === 1008;
if (isUnauthorized) {
const unauthorizedError = new UnauthorizedWebSocketConnectionError_default(event);
handleOpenError(unauthorizedError);
} else {
handleOpenError(event);
}
}
const openTimeout = setTimeout(() => {
const timeoutError = new WebSocketOpenTimeoutError(timeoutDuration);
handleOpenError(timeoutError);
}, timeoutDuration);
function handleOpenSuccess() {
removeAllSocketListeners();
clearTimeout(openTimeout);
resolve();
}
function handleSocketMessage(message) {
const hasValidAuth = message.data === "socket:auth:valid";
if (hasValidAuth) {
handleOpenSuccess();
}
}
if (waitForAuthentication) {
socket.addEventListener("message", handleSocketMessage);
} else {
socket.addEventListener("open", handleOpenSuccess);
}
socket.addEventListener("error", handleOpenError);
socket.addEventListener("close", handleClose);
});
}
async function closeClientSocket(socket, options = {}) {
const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT } = options;
const isAlreadyClosed = socket.readyState === socket.CLOSED;
if (isAlreadyClosed) {
return;
}
await new Promise((resolve, reject) => {
function removeAllSocketListeners() {
socket.removeEventListener("error", handleError);
socket.removeEventListener("close", handleClose);
}
function handleError(error) {
removeAllSocketListeners();
reject(error);
}
const closeTimeout = setTimeout(() => {
const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration);
handleError(timeoutError);
}, timeoutDuration);
function handleClose() {
removeAllSocketListeners();
clearTimeout(closeTimeout);
resolve();
}
socket.addEventListener("error", handleError);
socket.addEventListener("close", handleClose);
socket.close();
});
}
async function closeServerSocket(socket, options = {}) {
const { timeout: timeoutDuration = DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT } = options;
await new Promise((resolve, reject) => {
const closeTimeout = setTimeout(() => {
const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration);
reject(timeoutError);
}, timeoutDuration);
for (const client of socket.clients) {
client.terminate();
}
socket.close((error) => {
clearTimeout(closeTimeout);
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
// src/server/constants.ts
var ALLOWED_ACCESS_CONTROL_HTTP_METHODS = http.HTTP_METHODS.join(",");
var DEFAULT_ACCESS_CONTROL_HEADERS = Object.freeze({
"access-control-allow-origin": "*",
"access-control-allow-methods": ALLOWED_ACCESS_CONTROL_HTTP_METHODS,
"access-control-allow-headers": "*",
"access-control-expose-headers": "*",
"access-control-max-age": ""
});
var DEFAULT_PREFLIGHT_STATUS_CODE = 204;
var DEFAULT_HOSTNAME = "localhost";
var DEFAULT_LOG_UNHANDLED_REQUESTS = true;
// ../zimic-utils/dist/chunk-VPHA4ZCK.mjs
function createPathCharactersToEscapeRegex() {
return /([.(){}+$])/g;
}
function preparePathForRegex(path2) {
const pathURLPrefix = `data:${path2.startsWith("/") ? "" : "/"}`;
const pathAsURL = new URL(`${pathURLPrefix}${path2}`);
const encodedPath = pathAsURL.href.replace(pathURLPrefix, "");
return encodedPath.replace(/^\/+/g, "").replace(/\/+$/g, "").replace(createPathCharactersToEscapeRegex(), "\\$1");
}
function createPathParamRegex() {
return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)(?!\\[*+?])/gu;
}
function createRepeatingPathParamRegex() {
return /(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\\\+/gu;
}
function createOptionalPathParamRegex() {
return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\?(?<trailingSlash>\/)?/gu;
}
function createOptionalRepeatingPathParamRegex() {
return /(?<leadingSlash>\/)?(?<escape>\\)?:(?<identifier>[$_\p{ID_Start}][$\p{ID_Continue}]+)\*(?<trailingSlash>\/)?/gu;
}
function createRegexFromPath(path2) {
const pathRegexContent = preparePathForRegex(path2).replace(
createOptionalRepeatingPathParamRegex(),
(_match, leadingSlash, escape, identifier, trailingSlash) => {
if (escape) {
return `${leadingSlash ?? ""}:${identifier}\\*${trailingSlash ?? ""}`;
}
const hasSegmentBeforePrefix = leadingSlash === "/";
const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash;
const hasSegmentAfterSuffix = trailingSlash === "/";
const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash;
if (prefixExpression && suffixExpression) {
return `(?:${prefixExpression}(?<${identifier}>.+?)?${suffixExpression})?`;
} else if (prefixExpression) {
return `(?:${prefixExpression}(?<${identifier}>.+?))?`;
} else if (suffixExpression) {
return `(?:(?<${identifier}>.+?)${suffixExpression})?`;
} else {
return `(?<${identifier}>.+?)?`;
}
}
).replace(createRepeatingPathParamRegex(), (_match, escape, identifier) => {
return escape ? `:${identifier}\\+` : `(?<${identifier}>.+)`;
}).replace(
createOptionalPathParamRegex(),
(_match, leadingSlash, escape, identifier, trailingSlash) => {
if (escape) {
return `${leadingSlash ?? ""}:${identifier}\\?${trailingSlash ?? ""}`;
}
const hasSegmentBeforePrefix = leadingSlash === "/";
const prefixExpression = hasSegmentBeforePrefix ? "/?" : leadingSlash;
const hasSegmentAfterSuffix = trailingSlash === "/";
const suffixExpression = hasSegmentAfterSuffix ? "/?" : trailingSlash;
if (prefixExpression && suffixExpression) {
return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?)?${suffixExpression})`;
} else if (prefixExpression) {
return `(?:${prefixExpression}(?<${identifier}>[^\\/]+?))?`;
} else if (suffixExpression) {
return `(?:(?<${identifier}>[^\\/]+?)${suffixExpression})?`;
} else {
return `(?<${identifier}>[^\\/]+?)?`;
}
}
).replace(createPathParamRegex(), (_match, escape, identifier) => {
return escape ? `:${identifier}` : `(?<${identifier}>[^\\/]+?)`;
});
return new RegExp(`^/?${pathRegexContent}/?$`);
}
var createRegexFromPath_default = createRegexFromPath;
// ../zimic-utils/dist/url/excludeNonPathParams.mjs
function excludeNonPathParams(url) {
url.hash = "";
url.search = "";
url.username = "";
url.password = "";
return url;
}
var excludeNonPathParams_default = excludeNonPathParams;
// ../zimic-utils/dist/chunk-5UH44FTS.mjs
function isDefined(value) {
return value !== void 0 && value !== null;
}
var isDefined_default = isDefined;
// src/utils/arrays.ts
function removeArrayIndex(array, index) {
if (index >= 0 && index < array.length) {
array.splice(index, 1);
}
return array;
}
function removeArrayElement(array, element) {
const index = array.indexOf(element);
return removeArrayIndex(array, index);
}
// src/utils/environment.ts
function isClientSide() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
// src/utils/http.ts
var HTTP_METHODS_WITH_RESPONSE_BODY = /* @__PURE__ */ new Set([
"GET",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS"
]);
function methodCanHaveResponseBody(method) {
return HTTP_METHODS_WITH_RESPONSE_BODY.has(method);
}
// ../zimic-utils/dist/import/createCachedDynamicImport.mjs
function createCachedDynamicImport(importModuleDynamically) {
let cachedImportResult;
return async function importModuleDynamicallyWithCache() {
cachedImportResult ??= await importModuleDynamically();
return cachedImportResult;
};
}
var createCachedDynamicImport_default = createCachedDynamicImport;
// ../zimic-utils/dist/logging/Logger.mjs
var Logger = class _Logger {
prefix;
raw;
constructor(options = {}) {
const { prefix } = options;
this.prefix = prefix;
this.raw = prefix ? new _Logger({ ...options, prefix: void 0 }) : this;
}
logWithLevel(level, ...messages) {
if (this.prefix) {
console[level](this.prefix, ...messages);
} else {
console[level](...messages);
}
}
info(...messages) {
this.logWithLevel("log", ...messages);
}
warn(...messages) {
this.logWithLevel("warn", ...messages);
}
error(...messages) {
this.logWithLevel("error", ...messages);
}
table(headers, rows) {
const columnLengths = headers.map((header) => {
let maxValueLength = header.title.length;
for (const row of rows) {
const value = row[header.property];
if (value.length > maxValueLength) {
maxValueLength = value.length;
}
}
return maxValueLength;
});
const formattedRows = [];
const horizontalLine = columnLengths.map((length) => "\u2500".repeat(length));
formattedRows.push(horizontalLine, []);
for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) {
const header = headers[headerIndex];
const columnLength = columnLengths[headerIndex];
const value = header.title;
formattedRows.at(-1)?.push(value.padEnd(columnLength, " "));
}
formattedRows.push(horizontalLine);
for (const row of rows) {
formattedRows.push([]);
for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) {
const header = headers[headerIndex];
const columnLength = columnLengths[headerIndex];
const value = row[header.property];
formattedRows.at(-1)?.push(value.padEnd(columnLength, " "));
}
}
formattedRows.push(horizontalLine);
const formattedTable = formattedRows.map((row, index) => {
const isFirstLine = index === 0;
if (isFirstLine) {
return `\u250C\u2500${row.join("\u2500\u252C\u2500")}\u2500\u2510`;
}
const isLineAfterHeaders = index === 2;
if (isLineAfterHeaders) {
return `\u251C\u2500${row.join("\u2500\u253C\u2500")}\u2500\u2524`;
}
const isLastLine = index === formattedRows.length - 1;
if (isLastLine) {
return `\u2514\u2500${row.join("\u2500\u2534\u2500")}\u2500\u2518`;
}
return `\u2502 ${row.join(" \u2502 ")} \u2502`;
}).join("\n");
this.logWithLevel("log", formattedTable);
}
};
var Logger_default = Logger;
// src/utils/files.ts
var importFile = createCachedDynamicImport_default(
/* istanbul ignore next -- @preserve
* Ignoring as Node.js >=20 provides a global File and the import fallback won't run. */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
async () => globalThis.File ?? (await import('buffer')).File
);
var importFilesystem = createCachedDynamicImport_default(() => import('fs'));
async function pathExists(path2) {
const fs2 = await importFilesystem();
try {
await fs2.promises.access(path2);
return true;
} catch {
return false;
}
}
// src/utils/logging.ts
var logger = new Logger_default({
prefix: color3__default.default.cyan("[@zimic/interceptor]")
});
var importUtil = createCachedDynamicImport_default(() => import('util'));
async function formatValueToLog(value, options = {}) {
if (isClientSide()) {
return value;
}
const { colors = true } = options;
const util2 = await importUtil();
return util2.inspect(value, {
colors,
compact: true,
depth: Infinity,
maxArrayLength: Infinity,
maxStringLength: Infinity,
breakLength: Infinity,
sorted: true
});
}
// src/http/requestHandler/types/requests.ts
var HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES = Object.freeze(
/* @__PURE__ */ new Set([
"bodyUsed",
"arrayBuffer",
"blob",
"formData",
"json",
"text",
"clone"
])
);
var HTTP_INTERCEPTOR_RESPONSE_HIDDEN_PROPERTIES = Object.freeze(
new Set(
HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES
)
);
// src/http/interceptorWorker/constants.ts
var DEFAULT_UNHANDLED_REQUEST_STRATEGY = Object.freeze({
local: Object.freeze({
action: "reject",
log: true
}),
remote: Object.freeze({
action: "reject",
log: true
})
});
// src/http/interceptorWorker/HttpInterceptorWorker.ts
var HttpInterceptorWorker = class _HttpInterceptorWorker {
platform = null;
isRunning = false;
startingPromise;
stoppingPromise;
runningInterceptors = [];
async sharedStart(internalStart) {
if (this.isRunning) {
return;
}
if (this.startingPromise) {
return this.startingPromise;
}
try {
this.startingPromise = internalStart();
await this.startingPromise;
this.startingPromise = void 0;
} catch (error) {
if (!isClientSide()) {
console.error(error);
}
await this.stop();
throw error;
}
}
async sharedStop(internalStop) {
if (!this.isRunning) {
return;
}
if (this.stoppingPromise) {
return this.stoppingPromise;
}
const stoppingResult = internalStop();
if (stoppingResult instanceof Promise) {
this.stoppingPromise = stoppingResult;
await this.stoppingPromise;
}
this.stoppingPromise = void 0;
}
async logUnhandledRequestIfNecessary(request, strategy) {
if (strategy?.log) {
await _HttpInterceptorWorker.logUnhandledRequestWarning(request, strategy.action);
return { wasLogged: true };
}
return { wasLogged: false };
}
async getUnhandledRequestStrategy(request, interceptorType) {
const candidates = await this.getUnhandledRequestStrategyCandidates(request, interceptorType);
const strategy = this.reduceUnhandledRequestStrategyCandidates(candidates);
return strategy;
}
reduceUnhandledRequestStrategyCandidates(candidateStrategies) {
if (candidateStrategies.length === 0) {
return null;
}
return candidateStrategies.reduce(
(accumulatedStrategy, candidateStrategy) => ({
action: accumulatedStrategy.action,
log: accumulatedStrategy.log ?? candidateStrategy.log
})
);
}
async getUnhandledRequestStrategyCandidates(request, interceptorType) {
const globalDefaultStrategy = DEFAULT_UNHANDLED_REQUEST_STRATEGY[interceptorType];
try {
const interceptor = this.findInterceptorByRequestBaseURL(request);
if (!interceptor) {
return [];
}
const requestClone = request.clone();
const interceptorStrategy = await this.getInterceptorUnhandledRequestStrategy(requestClone, interceptor);
return [interceptorStrategy, globalDefaultStrategy].filter(isDefined_default);
} catch (error) {
console.error(error);
return [globalDefaultStrategy];
}
}
registerRunningInterceptor(interceptor) {
this.runningInterceptors.push(interceptor);
}
unregisterRunningInterceptor(interceptor) {
removeArrayElement(this.runningInterceptors, interceptor);
}
findInterceptorByRequestBaseURL(request) {
const interceptor = this.runningInterceptors.findLast((interceptor2) => {
return request.url.startsWith(interceptor2.baseURLAsString);
});
return interceptor;
}
async getInterceptorUnhandledRequestStrategy(request, interceptor) {
if (typeof interceptor.onUnhandledRequest === "function") {
const parsedRequest = await _HttpInterceptorWorker.parseRawUnhandledRequest(request);
return interceptor.onUnhandledRequest(parsedRequest);
}
return interceptor.onUnhandledRequest;
}
static createResponseFromDeclaration(request, declaration) {
const headers = new http.HttpHeaders(declaration.headers);
const status = declaration.status;
const canHaveBody = methodCanHaveResponseBody(request.method) && status !== 204;
if (!canHaveBody) {
return new Response(null, { headers, status });
}
if (typeof declaration.body === "string" || declaration.body === null || declaration.body === void 0 || declaration.body instanceof FormData || declaration.body instanceof URLSearchParams || declaration.body instanceof Blob || declaration.body instanceof ArrayBuffer || declaration.body instanceof ReadableStream) {
return new Response(declaration.body ?? null, { headers, status });
}
return Response.json(declaration.body, { headers, status });
}
static async parseRawUnhandledRequest(request) {
return this.parseRawRequest(
request
);
}
static async parseRawRequest(originalRawRequest, options) {
const rawRequest = originalRawRequest.clone();
const rawRequestClone = rawRequest.clone();
const parsedBody = await http.parseHttpBody(rawRequest).catch((error) => {
logger.error("Failed to parse request body:", error);
return null;
});
const headers = new http.HttpHeaders(rawRequest.headers);
const pathParams = this.parseRawPathParams(rawRequest, options);
const parsedURL = new URL(rawRequest.url);
const searchParams = new http.HttpSearchParams(parsedURL.searchParams);
const parsedRequest = new Proxy(rawRequest, {
has(target, property) {
if (_HttpInterceptorWorker.isHiddenRequestProperty(property)) {
return false;
}
return Reflect.has(target, property);
},
get(target, property) {
if (_HttpInterceptorWorker.isHiddenRequestProperty(property)) {
return void 0;
}
return Reflect.get(target, property, target);
}
});
Object.defineProperty(parsedRequest, "body", {
value: parsedBody,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "headers", {
value: headers,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "pathParams", {
value: pathParams,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "searchParams", {
value: searchParams,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "raw", {
value: rawRequestClone,
enumerable: true,
configurable: false,
writable: false
});
return parsedRequest;
}
static isHiddenRequestProperty(property) {
return HTTP_INTERCEPTOR_REQUEST_HIDDEN_PROPERTIES.has(property);
}
static async parseRawResponse(originalRawResponse) {
const rawResponse = originalRawResponse.clone();
const rawResponseClone = rawResponse.clone();
const parsedBody = await http.parseHttpBody(rawResponse).catch((error) => {
logger.error("Failed to parse response body:", error);
return null;
});
const headers = new http.HttpHeaders(rawResponse.headers);
const parsedRequest = new Proxy(rawResponse, {
has(target, property) {
if (_HttpInterceptorWorker.isHiddenResponseProperty(property)) {
return false;
}
return Reflect.has(target, property);
},
get(target, property) {
if (_HttpInterceptorWorker.isHiddenResponseProperty(property)) {
return void 0;
}
return Reflect.get(target, property, target);
}
});
Object.defineProperty(parsedRequest, "body", {
value: parsedBody,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "headers", {
value: headers,
enumerable: true,
configurable: false,
writable: false
});
Object.defineProperty(parsedRequest, "raw", {
value: rawResponseClone,
enumerable: true,
configurable: false,
writable: false
});
return parsedRequest;
}
static isHiddenResponseProperty(property) {
return HTTP_INTERCEPTOR_RESPONSE_HIDDEN_PROPERTIES.has(property);
}
static parseRawPathParams(request, options) {
const requestPath = request.url.replace(options?.baseURL ?? "", "");
const paramsMatch = options?.pathRegex.exec(requestPath);
const params = {};
for (const [paramName, paramValue] of Object.entries(paramsMatch?.groups ?? {})) {
params[paramName] = typeof paramValue === "string" ? decodeURIComponent(paramValue) : void 0;
}
return params;
}
static async logUnhandledRequestWarning(rawRequest, action) {
const request = await this.parseRawRequest(rawRequest);
const [formattedHeaders, formattedSearchParams, formattedBody] = await Promise.all([
formatValueToLog(request.headers.toObject()),
formatValueToLog(request.searchParams.toObject()),
formatValueToLog(request.body)
]);
logger[action === "bypass" ? "warn" : "error"](
`${action === "bypass" ? "Warning:" : "Error:"} Request was not handled and was ${action === "bypass" ? color3__default.default.yellow("bypassed") : color3__default.default.red("rejected")}.
`,
`${request.method} ${request.url}`,
"\n Headers:",
formattedHeaders,
"\n Search params:",
formattedSearchParams,
"\n Body:",
formattedBody,
"\n\nLearn more: https://zimic.dev/docs/interceptor/guides/http/unhandled-requests"
);
}
};
var HttpInterceptorWorker_default = HttpInterceptorWorker;
// src/utils/data.ts
function convertArrayBufferToBase64(buffer) {
if (isClientSide()) {
const bufferBytes = new Uint8Array(buffer);
const bufferAsStringArray = [];
for (const byte of bufferBytes) {
const byteCode = String.fromCharCode(byte);
bufferAsStringArray.push(byteCode);
}
const bufferAsString = bufferAsStringArray.join("");
return btoa(bufferAsString);
} else {
return Buffer.from(buffer).toString("base64");
}
}
function convertBase64ToArrayBuffer(base64Value) {
if (isClientSide()) {
const bufferAsString = atob(base64Value);
const array = Uint8Array.from(bufferAsString, (character) => character.charCodeAt(0));
return array.buffer;
} else {
return Buffer.from(base64Value, "base64");
}
}
var HEX_REGEX = /^[a-z0-9]+$/;
function convertHexLengthToByteLength(hexLength) {
return Math.ceil(hexLength / 2);
}
var BASE64URL_REGEX = /^[a-zA-Z0-9-_]+$/;
function convertHexLengthToBase64urlLength(hexLength) {
const byteLength = convertHexLengthToByteLength(hexLength);
return Math.ceil(byteLength * 4 / 3);
}
// src/utils/fetch.ts
async function serializeRequest(request) {
const requestClone = request.clone();
const serializedBody = requestClone.body ? convertArrayBufferToBase64(await requestClone.arrayBuffer()) : null;
return {
url: request.url,
method: request.method,
mode: request.mode,
headers: Object.fromEntries(request.headers),
cache: request.cache,
credentials: request.credentials,
integrity: request.integrity,
keepalive: request.keepalive,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: serializedBody
};
}
function deserializeResponse(serializedResponse) {
const deserializedBody = serializedResponse.body ? convertBase64ToArrayBuffer(serializedResponse.body) : null;
return new Response(deserializedBody, {
status: serializedResponse.status,
statusText: serializedResponse.statusText,
headers: new Headers(serializedResponse.headers)
});
}
// src/utils/crypto.ts
var importCrypto = createCachedDynamicImport_default(async () => {
const globalCrypto = globalThis.crypto;
return globalCrypto ?? await import('crypto');
});
// src/webSocket/constants.ts
var WEB_SOCKET_CONTROL_MESSAGES = Object.freeze(["socket:auth:valid"]);
// src/webSocket/errors/InvalidWebSocketMessageError.ts
var InvalidWebSocketMessageError = class extends Error {
constructor(message) {
super(`Web socket message is invalid and could not be parsed: ${message}`);
this.name = "InvalidWebSocketMessageError";
}
};
var InvalidWebSocketMessageError_default = InvalidWebSocketMessageError;
// src/webSocket/errors/NotRunningWebSocketHandlerError.ts
var NotRunningWebSocketHandlerError = class extends Error {
constructor() {
super("Web socket handler is not running.");
this.name = "NotRunningWebSocketHandlerError";
}
};
var NotRunningWebSocketHandlerError_default = NotRunningWebSocketHandlerError;
// src/webSocket/WebSocketHandler.ts
var WebSocketHandler = class {
sockets = /* @__PURE__ */ new Set();
socketTimeout;
messageTimeout;
channelListeners = {};
socketListeners = {
abortRequests: /* @__PURE__ */ new Map()
};
constructor(options) {
this.socketTimeout = options.socketTimeout ?? DEFAULT_WEB_SOCKET_LIFECYCLE_TIMEOUT;
this.messageTimeout = options.messageTimeout ?? DEFAULT_WEB_SOCKET_MESSAGE_TIMEOUT;
}
async registerSocket(socket, options = {}) {
const openPromise = waitForOpenClientSocket(socket, {
timeout: this.socketTimeout,
waitForAuthentication: options.waitForAuthentication
});
const handleSocketMessage = async (rawMessage) => {
await this.handleSocketMessage(socket, rawMessage);
};
socket.addEventListener("message", handleSocketMessage);
await openPromise;
function handleSocketError(error) {
console.error(error);
}
socket.addEventListener("error", handleSocketError);
const handleSocketClose = () => {
this.sockets.delete(socket);
this.emitSocket("abortRequests", socket);
this.socketListeners.abortRequests.delete(socket);
socket.removeEventListener("message", handleSocketMessage);
socket.removeEventListener("close", handleSocketClose);
socket.removeEventListener("error", handleSocketError);
};
socket.addEventListener("close", handleSocketClose);
this.sockets.add(socket);
}
handleSocketMessage = async (socket, rawMessage) => {
try {
if (this.isControlMessageData(rawMessage.data)) {
return;
}
const stringifiedMessageData = this.readRawMessageData(rawMessage.data);
const parsedMessageData = this.parseMessage(stringifiedMessageData);
await this.notifyListeners(parsedMessageData, socket);
} catch (error) {
console.error(error);
}
};
isControlMessageData(messageData) {
return typeof messageData === "string" && WEB_SOCKET_CONTROL_MESSAGES.includes(messageData);
}
readRawMessageData(data) {
if (typeof data === "string") {
return data;
} else {
throw new InvalidWebSocketMessageError_default(data);
}
}
parseMessage(stringifiedMessage) {
let parsedMessage;
try {
parsedMessage = JSON.parse(stringifiedMessage);
} catch {
throw new InvalidWebSocketMessageError_default(stringifiedMessage);
}
if (!this.isMessage(parsedMessage)) {
throw new InvalidWebSocketMessageError_default(stringifiedMessage);
}
if (this.isReplyMessage(parsedMessage)) {
return {
id: parsedMessage.id,
channel: parsedMessage.channel,
requestId: parsedMessage.requestId,
data: parsedMessage.data
};
}
return {
id: parsedMessage.id,
channel: parsedMessage.channel,
data: parsedMessage.data
};
}
isMessage(message) {
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");
}
isChannelEvent(event, channel) {
return event.channel === channel;
}
async notifyListeners(message, socket) {
if (this.isReplyMessage(message)) {
await this.notifyReplyListeners(message, socket);
} else {
await this.notifyEventListeners(message, socket);
}
}
async notifyReplyListeners(message, socket) {
const listeners = this.channelListeners[message.channel]?.reply ?? /* @__PURE__ */ new Set();
const listenerPromises = Array.from(listeners, async (listener) => {
await listener(message, socket);
});
await Promise.all(listenerPromises);
}
async notifyEventListeners(message, socket) {
const listeners = this.channelListeners[message.channel]?.event ?? /* @__PURE__ */ new Set();
const listenerPromises = Array.from(listeners, async (listener) => {
const replyData = await listener(message, socket);
await this.reply(message, replyData, { sockets: [socket] });
});
await Promise.all(listenerPromises);
}
async closeClientSockets(sockets = this.sockets) {
const closingPromises = Array.from(sockets, async (socket) => {
await closeClientSocket(socket, { timeout: this.socketTimeout });
});
await Promise.all(closingPromises);
}
async createEventMessage(channel, eventData) {
const crypto2 = await importCrypto();
const eventMessage = {
id: crypto2.randomUUID(),
channel,
data: eventData
};
return eventMessage;
}
async send(channel, eventData, options = {}) {
const event = await this.createEventMessage(channel, eventData);
this.sendMessage(event, options.sockets);
}
async request(channel, requestData, options = {}) {
const request = await this.createEventMessage(channel, requestData);
this.sendMessage(request, options.sockets);
const response = await this.waitForReply(channel, request, options.sockets);
return response.data;
}
async waitForReply(channel, request, sockets = this.sockets) {
return new Promise((resolve, reject) => {
const replyTimeout = setTimeout(() => {
this.offChannel("reply", channel, replyListener);
for (const socket of sockets) {
this.offSocket("abortRequests", socket, abortRequestsHandler);
}
const timeoutError = new WebSocketMessageTimeoutError(this.messageTimeout);
reject(timeoutError);
}, this.messageTimeout);
const replyListener = this.onChannel("reply", channel, (message) => {
if (message.requestId !== request.id) {
return;
}
clearTimeout(replyTimeout);
this.offChannel("reply", channel, replyListener);
for (const socket of sockets) {
this.offSocket("abortRequests", socket, abortRequestsHandler);
}
resolve(message);
});
const abortRequestsHandler = (options) => {
const shouldAbortRequest = options.shouldAbortRequest === void 0 || options.shouldAbortRequest(request);
if (!shouldAbortRequest) {
return;
}
clearTimeout(replyTimeout);
this.offChannel("reply", channel, replyListener);
for (const socket of sockets) {
this.offSocket("abortRequests", socket, abortRequestsHandler);
}
const abortError = new WebSocketMessageAbortError();
reject(abortError);
};
for (const socket of sockets) {
this.onSocket("abortRequests", socket, abortRequestsHandler);
}
});
}
isReplyMessage(message) {
return "requestId" in message;
}
async reply(request, replyData, options) {
const reply = await this.createReplyMessage(request, replyData);
if (this.isRunning) {
this.sendMessage(reply, options.sockets);
}
}
async createReplyMessage(request, replyData) {
const crypto2 = await importCrypto();
const replyMessage = {
id: crypto2.randomUUID(),
channel: request.channel,
requestId: request.id,
data: replyData
};
return replyMessage;
}
sendMessage(message, sockets = this.sockets) {
if (!this.isRunning) {
throw new NotRunningWebSocketHandlerError_default();
}
const stringifiedMessage = JSON.stringify(message);
for (const socket of sockets) {
socket.send(stringifiedMessage);
}
}
onChannel(type, channel, listener) {
const listeners = this.getOrCreateChannelListeners(channel);
listeners[type].add(listener);
return listener;
}
offAny() {
this.channelListeners = {};
for (const listenersBySocket of Object.values(this.socketListeners)) {
for (const listeners of listenersBySocket.values()) {
listeners.clear();
}
}
}
getOrCreateChannelListeners(channel) {
const listeners = this.channelListeners[channel] ?? {
event: /* @__PURE__ */ new Set(),
reply: /* @__PURE__ */ new Set()
};
if (!this.channelListeners[channel]) {
this.channelListeners[channel] = listeners;
}
return listeners;
}
offChannel(type, channel, listener) {
const listeners = this.channelListeners[channel];
listeners?.[type].delete(listener);
}
onSocket(type, socket, listener) {
const listeners = this.getOrCreateSocketListeners(type, socket);
listeners.add(listener);
return listener;
}
getOrCreateSocketListeners(type, socket) {
const listeners = this.socketListeners[type].get(socket) ?? /* @__PURE__ */ new Set();
if (!this.socketListeners[type].has(socket)) {
this.socketListeners[type].set(socket, listeners);
}
return listeners;
}
offSocket(type, socket, listener) {
const listeners = this.socketListeners[type].get(socket);
listeners?.delete(listener);
}
emitSocket(type, socket, options = {}) {
for (const listener of this.socketListeners[type].get(socket) ?? []) {
listener(options);
}
}
};
var WebSocketHandler_default = WebSocketHandler;
// src/webSocket/WebSocketServer.ts
var { WebSocketServer: ServerSocket } = ClientSocket__default.default;
var WebSocketServer = class extends WebSocketHandler_default {
webSocketServer;
httpServer;
authenticate;
constructor(options) {
super({
socketTimeout: options.socketTimeout,
messageTimeout: options.messageTimeout
});
this.httpServer = options.httpServer;
this.authenticate = options.authenticate;
}
get isRunning() {
return this.webSocketServer !== void 0;
}
start() {
if (this.isRunning) {
return;
}
const webSocketServer = new ServerSocket({ server: this.httpServer });
webSocketServer.on("error", (error) => {
console.error(error);
});
webSocketServer.on("connection", async (socket, request) => {
if (this.authenticate) {
const result = await this.authenticate(socket, request);
if (!result.isValid) {
socket.close(1008, result.message);
return;
}
}
try {
await super.registerSocket(socket);
socket.send("socket:auth:valid");
} catch (error) {
webSocketServer.emit("error", error);
}
});
this.webSocketServer = webSocketServer;
}
async stop() {
if (!this.webSocketServer || !this.isRunning) {
return;
}
super.offAny();
await super.closeClientSockets();
await closeServerSocket(this.webSocketServer, { timeout: this.socketTimeout });
this.webSocketServer = void 0;
}
};
var WebSocketServer_default = WebSocketServer;
// src/server/errors/InvalidInterceptorTokenError.ts
var InvalidInterceptorTokenError = class extends Error {
constructor(tokenId) {
super(`Invalid interceptor token: ${tokenId}`);
this.name = "InvalidInterceptorTokenError";
}
};
var InvalidInterceptorTokenError_default = InvalidInterceptorTokenError;
// src/server/errors/InvalidInterceptorTokenFileError.ts
var InvalidInterceptorTokenFileError = class extends Error {
constructor(tokenFilePath, validationErrorMessage) {
super(`Invalid interceptor token file ${tokenFilePath}: ${validationErrorMessage}`);
this.name = "InvalidInterceptorTokenFileError";
}
};
var InvalidInterceptorTokenFileError_default = InvalidInterceptorTokenFileError;
// src/server/errors/InvalidInterceptorTokenValueError.ts
var InvalidInterceptorTokenValueError = class extends Error {
constructor(tokenValue) {
super(`Invalid interceptor token value: ${tokenValue}`);
this.name = "InvalidInterceptorTokenValueError";
}
};
var InvalidInterceptorTokenValueError_default = InvalidInterceptorTokenValueError;
// src/server/utils/auth.ts
var DEFAULT_INTERCEPTOR_TOKENS_DIRECTORY = path__default.default.join(
".zimic",
"interceptor",
"server",
`tokens${""}`
);
var INTERCEPTOR_TOKEN_ID_HEX_LENGTH = 32;
var INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH = 64;
var INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH = INTERCEPTOR_TOKEN_ID_HEX_LENGTH + INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH;
var INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH = convertHexLengthToBase64urlLength(
INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH
);
var INTERCEPTOR_TOKEN_SALT_HEX_LENGTH = 64;
var INTERCEPTOR_TOKEN_HASH_ITERATIONS = Number("1000000");
var INTERCEPTOR_TOKEN_HASH_HEX_LENGTH = 128;
var INTERCEPTOR_TOKEN_HASH_ALGORITHM = "sha512";
var pbkdf2 = util__default.default.promisify(crypto__default.default.pbkdf2);
async function hashInterceptorToken(plainToken, salt) {
const hashBuffer = await pbkdf2(
plainToken,
salt,
INTERCEPTOR_TOKEN_HASH_ITERATIONS,
convertHexLengthToByteLength(INTERCEPTOR_TOKEN_HASH_HEX_LENGTH),
INTERCEPTOR_TOKEN_HASH_ALGORITHM
);
const hash = hashBuffer.toString("hex");
return hash;
}
function createInterceptorTokenId() {
return crypto__default.default.randomUUID().replace(/[^a-z0-9]/g, "");
}
function isValidInterceptorTokenId(tokenId) {
return tokenId.length === INTERCEPTOR_TOKEN_ID_HEX_LENGTH && HEX_REGEX.test(tokenId);
}
function isValidInterceptorTokenValue(tokenValue) {
return tokenValue.length === INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH && BASE64URL_REGEX.test(tokenValue);
}
async function createInterceptorTokensDirectory(tokensDirectory) {
try {
const parentTokensDirectory = path__default.default.dirname(tokensDirectory);
await fs__default.default.promises.mkdir(parentTokensDirectory, { recursive: true });
await fs__default.default.promises.mkdir(tokensDirectory, { mode: 448, recursive: true });
await fs__default.default.promises.appendFile(path__default.default.join(tokensDirectory, ".gitignore"), `*${os__default.default.EOL}`, { encoding: "utf-8" });
} catch (error) {
logger.error(
`${color3__default.default.red(color3__default.default.bold("\u2716"))} Failed to create the tokens directory: ${color3__default.default.magenta(tokensDirectory)}`
);
throw error;
}
}
var interceptorTokenFileContentSchema = z__namespace.object({
version: z__namespace.literal(1),
token: z__namespace.object({
id: z__namespace.string().length(INTERCEPTOR_TOKEN_ID_HEX_LENGTH).regex(HEX_REGEX),
name: z__namespace.string().optional(),
secret: z__namespace.object({
hash: z__namespace.string().length(INTERCEPTOR_TOKEN_HASH_HEX_LENGTH).regex(HEX_REGEX),
salt: z__namespace.string().length(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH).regex(HEX_REGEX)
}),
createdAt: z__namespace.iso.datetime().transform((value) => new Date(value))
})
});
async function saveInterceptorTokenToFile(tokensDirectory, token) {
const tokeFilePath = path__default.default.join(tokensDirectory, token.id);
const persistedToken = {
id: token.id,
name: token.name,
secret: {
hash: token.secret.hash,
salt: token.secret.salt
},
createdAt: token.createdAt.toISOString()
};
const tokenFileContent = interceptorTokenFileContentSchema.parse({
version: 1,
token: persistedToken
});
await fs__default.default.promises.writeFile(tokeFilePath, JSON.stringify(tokenFileContent, null, 2), {
mode: 384,
encoding: "utf-8"
});
return tokeFilePath;
}
async function readInterceptorTokenFromFile(tokenId, options) {
if (!isValidInterceptorTokenId(tokenId)) {
throw new InvalidInterceptorTokenError_default(tokenId);
}
const tokenFilePath = path__default.default.join(options.tokensDirectory, tokenId);
const tokenFileExists = await pathExists(tokenFilePath);
if (!tokenFileExists) {
return null;
}
const tokenFileContentAsString = await fs__default.default.promises.readFile(tokenFilePath, { encoding: "utf-8" });
const validation = interceptorTokenFileContentSchema.safeParse(JSON.parse(tokenFileContentAsString));
if (!validation.success) {
throw new InvalidInterceptorTokenFileError_default(tokenFilePath, validation.error.message);
}
return validation.data.token;
}
async function createInterceptorToken(options = {}) {
const { name, tokensDirectory = DEFAULT_INTERCEPTOR_TOKENS_DIRECTORY } = options;
const tokensDirectoryExists = await pathExists(tokensDirectory);
if (!tokensDirectoryExists) {
await createInterceptorTokensDirectory(tokensDirectory);
}
const tokenId = createInterceptorTokenId();
if (!isValidInterceptorTokenId(tokenId)) {
throw new InvalidInterceptorTokenError_default(tokenId);
}
const tokenSecretSizeInBytes = convertHexLengthToByteLength(INTERCEPTOR_TOKEN_SECRET_HEX_LENGTH);
const tokenSecret = crypto__default.default.randomBytes(tokenSecretSizeInBytes).toString("hex");
const tokenSecretSaltSizeInBytes = convertHexLengthToByteLength(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH);
const tokenSecretSalt = crypto__default.default.randomBytes(tokenSecretSaltSizeInBytes).toString("hex");
const tokenSecretHash = await hashInterceptorToken(tokenSecret, tokenSecretSalt);
const tokenValue = Buffer.from(`${tokenId}${tokenSecret}`, "hex").toString("base64url");
if (!isValidInterceptorTokenValue(tokenValue)) {
throw new InvalidInterceptorTokenValueError_default(tokenValue);
}
const token = {
id: tokenId,
name,
secret: {
hash: tokenSecretHash,
salt: tokenSecretSalt,
value: tokenSecret
},
value: tokenValue,
createdAt: /* @__PURE__ */ new Date()
};
await saveInterceptorTokenToFile(tokensDirectory, token);
return token;
}
async function listInterceptorTokens(options = {}) {
const { tokensDirectory = DEFAULT_INTERCEPTOR_TOKENS_DIRECTORY } = options;
const tokensDirectoryExists = await pathExists(tokensDirectory);
if (!tokensDirectoryExists) {
return [];
}
const files = await fs__default.default.promises.readdir(tokensDirectory);
const tokenReadPromises = files.map(async (file) => {
if (!isValidInterceptorTokenId(file)) {
return null;
}
const tokenId = file;
const token = await readInterceptorTokenFromFile(tokenId, { tokensDirectory });
return token;
});
const tokenCandidates = await Promise.allSettled(tokenReadPromises);
const tokens = [];
for (const tokenCandidate of tokenCandidates) {
if (tokenCandidate.status === "rejected") {
console.error(tokenCandidate.reason);
} else if (tokenCandidate.value !== null) {
tokens.push(tokenCandidate.value);
}
}
tokens.sort((token, otherToken) => token.createdAt.getTime() - otherToken.createdAt.getTime());
return tokens;
}
async function validateInterceptorToken(tokenValue, options) {
if (!isValidInterceptorTokenValue(tokenValue)) {
throw new InvalidInterceptorTokenValueError_default(tokenValue);
}
const decodedTokenValue = Buffer.from(tokenValue, "base64url").toString("hex");
const tokenId = decodedTokenValue.slice(0, INTERCEPTOR_TOKEN_ID_HEX_LENGTH);
const tokenSecret = decodedTokenValue.slice(
INTERCEPTOR_TOKEN_ID_HEX_LENGTH,
INTERCEPTOR_TOKEN_ID_HEX_LENGTH + INTERCEPTOR_TOKEN_VALUE_HEX_LENGTH
);
const tokenFromFile = await readInterceptorTokenFromFile(tokenId, options);
if (!tokenFromFile) {
throw new InvalidInterceptorTokenValueError_default(tokenValue);
}
const tokenSecretHash = await hashInterceptorToken(tokenSecret, tokenFromFile.secret.salt);
if (tokenSecretHash !== toke