@zimic/interceptor
Version:
Next-gen TypeScript-first HTTP intercepting and mocking
1,452 lines (1,417 loc) • 66.1 kB
JavaScript
import { __name } from './chunk-CGILA3WO.mjs';
import { HTTP_METHODS, HttpHeaders, HttpSearchParams, HttpFormData } from '@zimic/http';
import { normalizeNodeRequest, sendNodeResponse } from '@whatwg-node/server';
import { createServer } from 'http';
import color3 from 'picocolors';
import ClientSocket from 'isomorphic-ws';
import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';
import util from 'util';
import * as z from 'zod';
// src/server/errors/RunningInterceptorServerError.ts
var RunningInterceptorServerError = class extends Error {
static {
__name(this, "RunningInterceptorServerError");
}
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 {
static {
__name(this, "NotRunningInterceptorServerError");
}
constructor() {
super("The interceptor server is not running. Did you forget to start it?");
this.name = "NotRunningInterceptorServerError";
}
};
var NotRunningInterceptorServerError_default = NotRunningInterceptorServerError;
// src/utils/http.ts
var HttpServerTimeoutError = class extends Error {
static {
__name(this, "HttpServerTimeoutError");
}
};
var HttpServerStartTimeoutError = class extends HttpServerTimeoutError {
static {
__name(this, "HttpServerStartTimeoutError");
}
constructor(reachedTimeout) {
super(`HTTP server start timed out after ${reachedTimeout}ms.`);
this.name = "HttpServerStartTimeout";
}
};
var HttpServerStopTimeoutError = class extends HttpServerTimeoutError {
static {
__name(this, "HttpServerStopTimeoutError");
}
constructor(reachedTimeout) {
super(`HTTP server stop timed out after ${reachedTimeout}ms.`);
this.name = "HttpServerStopTimeout";
}
};
var DEFAULT_HTTP_SERVER_LIFECYCLE_TIMEOUT = 60 * 1e3;
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);
}
__name(handleStartError, "handleStartError");
const startTimeout = setTimeout(() => {
const timeoutError = new HttpServerStartTimeoutError(timeoutDuration);
handleStartError(timeoutError);
}, timeoutDuration);
function handleStartSuccess() {
server.off("error", handleStartError);
clearTimeout(startTimeout);
resolve();
}
__name(handleStartSuccess, "handleStartSuccess");
server.once("error", handleStartError);
server.listen(port, hostname, handleStartSuccess);
});
}
__name(startHttpServer, "startHttpServer");
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();
});
}
__name(stopHttpServer, "stopHttpServer");
function getHttpServerPort(server) {
const address = server.address();
if (typeof address === "string") {
return void 0;
} else {
return address?.port;
}
}
__name(getHttpServerPort, "getHttpServerPort");
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);
}
__name(methodCanHaveResponseBody, "methodCanHaveResponseBody");
// src/webSocket/errors/UnauthorizedWebSocketConnectionError.ts
var UnauthorizedWebSocketConnectionError = class extends Error {
constructor(event) {
super(`${event.reason} (code ${event.code})`);
this.event = event;
this.name = "UnauthorizedWebSocketConnectionError";
}
static {
__name(this, "UnauthorizedWebSocketConnectionError");
}
};
var UnauthorizedWebSocketConnectionError_default = UnauthorizedWebSocketConnectionError;
// src/utils/webSocket.ts
var WebSocketTimeoutError = class extends Error {
static {
__name(this, "WebSocketTimeoutError");
}
};
var WebSocketOpenTimeoutError = class extends WebSocketTimeoutError {
static {
__name(this, "WebSocketOpenTimeoutError");
}
constructor(reachedTimeout) {
super(`Web socket open timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketOpenTimeout";
}
};
var WebSocketMessageTimeoutError = class extends WebSocketTimeoutError {
static {
__name(this, "WebSocketMessageTimeoutError");
}
constructor(reachedTimeout) {
super(`Web socket message timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketMessageTimeout";
}
};
var WebSocketMessageAbortError = class extends WebSocketTimeoutError {
static {
__name(this, "WebSocketMessageAbortError");
}
constructor() {
super("Web socket message was aborted.");
this.name = "WebSocketMessageAbortError";
}
};
var WebSocketCloseTimeoutError = class extends WebSocketTimeoutError {
static {
__name(this, "WebSocketCloseTimeoutError");
}
constructor(reachedTimeout) {
super(`Web socket close timed out after ${reachedTimeout}ms.`);
this.name = "WebSocketCloseTimeout";
}
};
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);
}
__name(removeAllSocketListeners, "removeAllSocketListeners");
function handleOpenError(error) {
removeAllSocketListeners();
reject(error);
}
__name(handleOpenError, "handleOpenError");
function handleClose(event) {
const isUnauthorized = event.code === 1008;
if (isUnauthorized) {
const unauthorizedError = new UnauthorizedWebSocketConnectionError_default(event);
handleOpenError(unauthorizedError);
} else {
handleOpenError(event);
}
}
__name(handleClose, "handleClose");
const openTimeout = setTimeout(() => {
const timeoutError = new WebSocketOpenTimeoutError(timeoutDuration);
handleOpenError(timeoutError);
}, timeoutDuration);
function handleOpenSuccess() {
removeAllSocketListeners();
clearTimeout(openTimeout);
resolve();
}
__name(handleOpenSuccess, "handleOpenSuccess");
function handleSocketMessage(message) {
const hasValidAuth = message.data === "socket:auth:valid";
if (hasValidAuth) {
handleOpenSuccess();
}
}
__name(handleSocketMessage, "handleSocketMessage");
if (waitForAuthentication) {
socket.addEventListener("message", handleSocketMessage);
} else {
socket.addEventListener("open", handleOpenSuccess);
}
socket.addEventListener("error", handleOpenError);
socket.addEventListener("close", handleClose);
});
}
__name(waitForOpenClientSocket, "waitForOpenClientSocket");
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);
}
__name(removeAllSocketListeners, "removeAllSocketListeners");
function handleError(error) {
removeAllSocketListeners();
reject(error);
}
__name(handleError, "handleError");
const closeTimeout = setTimeout(() => {
const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration);
handleError(timeoutError);
}, timeoutDuration);
function handleClose() {
removeAllSocketListeners();
clearTimeout(closeTimeout);
resolve();
}
__name(handleClose, "handleClose");
socket.addEventListener("error", handleError);
socket.addEventListener("close", handleClose);
socket.close();
});
}
__name(closeClientSocket, "closeClientSocket");
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();
}
});
});
}
__name(closeServerSocket, "closeServerSocket");
// src/server/constants.ts
var ALLOWED_ACCESS_CONTROL_HTTP_METHODS = 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-2D3UJWOA.mjs
var __defProp = Object.defineProperty;
var __name2 = /* @__PURE__ */ __name((target, value) => __defProp(target, "name", { value, configurable: true }), "__name");
// ../zimic-utils/dist/chunk-4RR2YNYT.mjs
var URL_PATH_PARAM_REGEX = /\/:([^/]+)/g;
function createRegExpFromURL(url) {
URL_PATH_PARAM_REGEX.lastIndex = 0;
const urlWithReplacedPathParams = encodeURI(url).replace(/([.()*?+$\\])/g, "\\$1").replace(URL_PATH_PARAM_REGEX, "/(?<$1>[^/]+)").replace(/^(\/)|(\/)$/g, "");
return new RegExp(`^(?:/)?${urlWithReplacedPathParams}(?:/)?$`);
}
__name(createRegExpFromURL, "createRegExpFromURL");
__name2(createRegExpFromURL, "createRegExpFromURL");
var createRegExpFromURL_default = createRegExpFromURL;
// ../zimic-utils/dist/url/excludeURLParams.mjs
function excludeURLParams(url) {
url.hash = "";
url.search = "";
url.username = "";
url.password = "";
return url;
}
__name(excludeURLParams, "excludeURLParams");
__name2(excludeURLParams, "excludeURLParams");
var excludeURLParams_default = excludeURLParams;
// ../zimic-utils/dist/chunk-WC2DBWWR.mjs
function isDefined(value) {
return value !== void 0 && value !== null;
}
__name(isDefined, "isDefined");
__name2(isDefined, "isDefined");
var isDefined_default = isDefined;
// src/utils/arrays.ts
function removeArrayIndex(array, index) {
if (index >= 0 && index < array.length) {
array.splice(index, 1);
}
return array;
}
__name(removeArrayIndex, "removeArrayIndex");
function removeArrayElement(array, element) {
const index = array.indexOf(element);
return removeArrayIndex(array, index);
}
__name(removeArrayElement, "removeArrayElement");
// src/utils/environment.ts
function isClientSide() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
__name(isClientSide, "isClientSide");
// ../zimic-utils/dist/import/createCachedDynamicImport.mjs
function createCachedDynamicImport(importModuleDynamically) {
let cachedImportResult;
return /* @__PURE__ */ __name2(/* @__PURE__ */ __name(async function importModuleDynamicallyWithCache() {
cachedImportResult ??= await importModuleDynamically();
return cachedImportResult;
}, "importModuleDynamicallyWithCache"), "importModuleDynamicallyWithCache");
}
__name(createCachedDynamicImport, "createCachedDynamicImport");
__name2(createCachedDynamicImport, "createCachedDynamicImport");
var createCachedDynamicImport_default = createCachedDynamicImport;
// ../zimic-utils/dist/logging/Logger.mjs
var Logger = class _Logger {
static {
__name(this, "_Logger");
}
static {
__name2(this, "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;
}
}
__name(pathExists, "pathExists");
// src/utils/logging.ts
var logger = new Logger_default({
prefix: color3.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
});
}
__name(formatValueToLog, "formatValueToLog");
// 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/errors/InvalidFormDataError.ts
var InvalidFormDataError = class extends SyntaxError {
static {
__name(this, "InvalidFormDataError");
}
constructor(value) {
super(`Failed to parse value as form data: ${value}`);
this.name = "InvalidFormDataError";
}
};
var InvalidFormDataError_default = InvalidFormDataError;
// src/http/interceptorWorker/errors/InvalidJSONError.ts
var InvalidJSONError = class extends SyntaxError {
static {
__name(this, "InvalidJSONError");
}
constructor(value) {
super(`Failed to parse value as JSON: ${value}`);
this.name = "InvalidJSONError";
}
};
var InvalidJSONError_default = InvalidJSONError;
// src/http/interceptorWorker/HttpInterceptorWorker.ts
var HttpInterceptorWorker = class _HttpInterceptorWorker {
static {
__name(this, "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 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 === 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 this.parseRawBody(rawRequest);
const headers = new HttpHeaders(rawRequest.headers);
const pathParams = options.urlRegex ? this.parseRawPathParams(options.urlRegex, rawRequest) : {};
const parsedURL = new URL(rawRequest.url);
const searchParams = new 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 this.parseRawBody(rawResponse);
const headers = new 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(matchedURLRegex, request) {
const match = request.url.match(matchedURLRegex);
const pathParams = { ...match?.groups };
return pathParams;
}
static async parseRawBody(resource) {
const contentType = resource.headers.get("content-type");
try {
if (contentType) {
if (contentType.startsWith("application/json")) {
return await this.parseRawBodyAsJSON(resource);
}
if (contentType.startsWith("multipart/form-data")) {
return await this.parseRawBodyAsFormData(resource);
}
if (contentType.startsWith("application/x-www-form-urlencoded")) {
return await this.parseRawBodyAsSearchParams(resource);
}
if (contentType.startsWith("text/") || contentType.startsWith("application/xml")) {
return await this.parseRawBodyAsText(resource);
}
if (contentType.startsWith("application/") || contentType.startsWith("image/") || contentType.startsWith("audio/") || contentType.startsWith("font/") || contentType.startsWith("video/") || contentType.startsWith("multipart/")) {
return await this.parseRawBodyAsBlob(resource);
}
}
const resourceClone = resource.clone();
try {
return await this.parseRawBodyAsJSON(resource);
} catch {
return await this.parseRawBodyAsBlob(resourceClone);
}
} catch (error) {
console.error(error);
return null;
}
}
static async parseRawBodyAsJSON(resource) {
const bodyAsText = await resource.text();
if (!bodyAsText.trim()) {
return null;
}
try {
const bodyAsJSON = JSON.parse(bodyAsText);
return bodyAsJSON;
} catch {
throw new InvalidJSONError_default(bodyAsText);
}
}
static async parseRawBodyAsSearchParams(resource) {
const bodyAsText = await resource.text();
if (!bodyAsText.trim()) {
return null;
}
const bodyAsSearchParams = new HttpSearchParams(bodyAsText);
return bodyAsSearchParams;
}
static async parseRawBodyAsFormData(resource) {
const resourceClone = resource.clone();
try {
const bodyAsRawFormData = await resource.formData();
const bodyAsFormData = new HttpFormData();
for (const [key, value] of bodyAsRawFormData) {
bodyAsFormData.append(key, value);
}
return bodyAsFormData;
} catch {
const bodyAsText = await resourceClone.text();
if (!bodyAsText.trim()) {
return null;
}
throw new InvalidFormDataError_default(bodyAsText);
}
}
static async parseRawBodyAsBlob(resource) {
const bodyAsBlob = await resource.blob();
return bodyAsBlob;
}
static async parseRawBodyAsText(resource) {
const bodyAsText = await resource.text();
return bodyAsText || null;
}
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.yellow("bypassed") : color3.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");
}
}
__name(convertArrayBufferToBase64, "convertArrayBufferToBase64");
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");
}
}
__name(convertBase64ToArrayBuffer, "convertBase64ToArrayBuffer");
var HEX_REGEX = /^[a-z0-9]+$/;
function convertHexLengthToByteLength(hexLength) {
return Math.ceil(hexLength / 2);
}
__name(convertHexLengthToByteLength, "convertHexLengthToByteLength");
var BASE64URL_REGEX = /^[a-zA-Z0-9-_]+$/;
function convertHexLengthToBase64urlLength(hexLength) {
const byteLength = convertHexLengthToByteLength(hexLength);
return Math.ceil(byteLength * 4 / 3);
}
__name(convertHexLengthToBase64urlLength, "convertHexLengthToBase64urlLength");
// 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
};
}
__name(serializeRequest, "serializeRequest");
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)
});
}
__name(deserializeResponse, "deserializeResponse");
// 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/InvalidWebSocketMessage.ts
var InvalidWebSocketMessage = class extends Error {
static {
__name(this, "InvalidWebSocketMessage");
}
constructor(message) {
super(`Web socket message is invalid and could not be parsed: ${message}`);
this.name = "InvalidWebSocketMessage";
}
};
var InvalidWebSocketMessage_default = InvalidWebSocketMessage;
// src/webSocket/errors/NotRunningWebSocketHandlerError.ts
var NotRunningWebSocketHandlerError = class extends Error {
static {
__name(this, "NotRunningWebSocketHandlerError");
}
constructor() {
super("Web socket handler is not running.");
this.name = "NotRunningWebSocketHandlerError";
}
};
var NotRunningWebSocketHandlerError_default = NotRunningWebSocketHandlerError;
// src/webSocket/WebSocketHandler.ts
var WebSocketHandler = class {
static {
__name(this, "WebSocketHandler");
}
sockets = /* @__PURE__ */ new Set();
socketTimeout;
messageTimeout;
channelListeners = {};
socketListeners = {
messageAbort: /* @__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 = /* @__PURE__ */ __name(async (rawMessage) => {
await this.handleSocketMessage(socket, rawMessage);
}, "handleSocketMessage");
socket.addEventListener("message", handleSocketMessage);
await openPromise;
function handleSocketError(error) {
console.error(error);
}
__name(handleSocketError, "handleSocketError");
socket.addEventListener("error", handleSocketError);
const handleSocketClose = /* @__PURE__ */ __name(() => {
socket.removeEventListener("message", handleSocketMessage);
socket.removeEventListener("close", handleSocketClose);
socket.removeEventListener("error", handleSocketError);
this.removeSocket(socket);
}, "handleSocketClose");
socket.addEventListener("close", handleSocketClose);
this.sockets.add(socket);
}
handleSocketMessage = /* @__PURE__ */ __name(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);
}
}, "handleSocketMessage");
isControlMessageData(messageData) {
return typeof messageData === "string" && WEB_SOCKET_CONTROL_MESSAGES.includes(messageData);
}
readRawMessageData(data) {
if (typeof data === "string") {
return data;
} else {
throw new InvalidWebSocketMessage_default(data);
}
}
parseMessage(stringifiedMessage) {
let parsedMessage;
try {
parsedMessage = JSON.parse(stringifiedMessage);
} catch {
throw new InvalidWebSocketMessage_default(stringifiedMessage);
}
if (!this.isMessage(parsedMessage)) {
throw new InvalidWebSocketMessage_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");
}
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);
}
removeSocket(socket) {
this.abortSocketMessages([socket]);
this.sockets.delete(socket);
}
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.id, options.sockets);
return response.data;
}
async waitForReply(channel, requestId, sockets = this.sockets) {
return new Promise((resolve, reject) => {
const replyTimeout = setTimeout(() => {
this.offReply(channel, replyListener);
this.offAbortSocketMessages(sockets, abortListener);
const timeoutError = new WebSocketMessageTimeoutError(this.messageTimeout);
reject(timeoutError);
}, this.messageTimeout);
const abortListener = this.onAbortSocketMessages(sockets, (error) => {
clearTimeout(replyTimeout);
this.offReply(channel, replyListener);
this.offAbortSocketMessages(sockets, abortListener);
reject(error);
});
const replyListener = this.onReply(channel, (message) => {
if (message.requestId === requestId) {
clearTimeout(replyTimeout);
this.offReply(channel, replyListener);
this.offAbortSocketMessages(sockets, abortListener);
resolve(message);
}
});
});
}
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);
}
}
onEvent(channel, listener) {
const listeners = this.getOrCreateChannelListeners(channel);
listeners.event.add(listener);
return listener;
}
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;
}
onReply(channel, listener) {
const listeners = this.getOrCreateChannelListeners(channel);
listeners.reply.add(listener);
return listener;
}
offEvent(channel, listener) {
this.channelListeners[channel]?.event.delete(listener);
}
offReply(channel, listener) {
this.channelListeners[channel]?.reply.delete(listener);
}
removeAllChannelListeners() {
this.channelListeners = {};
}
onAbortSocketMessages(sockets, listener) {
for (const socket of sockets) {
let listeners = this.socketListeners.messageAbort.get(socket);
if (!listeners) {
listeners = /* @__PURE__ */ new Set();
this.socketListeners.messageAbort.set(socket, listeners);
}
listeners.add(listener);
}
return listener;
}
offAbortSocketMessages(sockets, listener) {
for (const socket of sockets) {
this.socketListeners.messageAbort.get(socket)?.delete(listener);
}
}
abortSocketMessages(sockets = this.sockets) {
const abortError = new WebSocketMessageAbortError();
for (const socket of sockets) {
const listeners = this.socketListeners.messageAbort.get(socket) ?? [];
for (const listener of listeners) {
listener(abortError);
}
}
}
};
var WebSocketHandler_default = WebSocketHandler;
// src/webSocket/WebSocketServer.ts
var { WebSocketServer: ServerSocket } = ClientSocket;
var WebSocketServer = class extends WebSocketHandler_default {
static {
__name(this, "WebSocketServer");
}
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.removeAllChannelListeners();
super.abortSocketMessages();
await super.closeClientSockets();
await closeServerSocket(this.webSocketServer, { timeout: this.socketTimeout });
this.webSocketServer.removeAllListeners();
this.webSocketServer = void 0;
}
};
var WebSocketServer_default = WebSocketServer;
// src/server/errors/InvalidInterceptorTokenError.ts
var InvalidInterceptorTokenError = class extends Error {
static {
__name(this, "InvalidInterceptorTokenError");
}
constructor(tokenId) {
super(`Invalid interceptor token: ${tokenId}`);
this.name = "InvalidInterceptorTokenError";
}
};
var InvalidInterceptorTokenError_default = InvalidInterceptorTokenError;
// src/server/errors/InvalidInterceptorTokenFileError.ts
var InvalidInterceptorTokenFileError = class extends Error {
static {
__name(this, "InvalidInterceptorTokenFileError");
}
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 {
static {
__name(this, "InvalidInterceptorTokenValueError");
}
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.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.promisify(crypto.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;
}
__name(hashInterceptorToken, "hashInterceptorToken");
function createInterceptorTokenId() {
return crypto.randomUUID().replace(/[^a-z0-9]/g, "");
}
__name(createInterceptorTokenId, "createInterceptorTokenId");
function isValidInterceptorTokenId(tokenId) {
return tokenId.length === INTERCEPTOR_TOKEN_ID_HEX_LENGTH && HEX_REGEX.test(tokenId);
}
__name(isValidInterceptorTokenId, "isValidInterceptorTokenId");
function isValidInterceptorTokenValue(tokenValue) {
return tokenValue.length === INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH && BASE64URL_REGEX.test(tokenValue);
}
__name(isValidInterceptorTokenValue, "isValidInterceptorTokenValue");
async function createInterceptorTokensDirectory(tokensDirectory) {
try {
const parentTokensDirectory = path.dirname(tokensDirectory);
await fs.promises.mkdir(parentTokensDirectory, { recursive: true });
await fs.promises.mkdir(tokensDirectory, { mode: 448, recursive: true });
await fs.promises.appendFile(path.join(tokensDirectory, ".gitignore"), `*${os.EOL}`, { encoding: "utf-8" });
} catch (error) {
logger.error(
`${color3.red(color3.bold("\u2716"))} Failed to create the tokens directory: ${color3.magenta(tokensDirectory)}`
);
throw error;
}
}
__name(createInterceptorTokensDirectory, "createInterceptorTokensDirectory");
var interceptorTokenFileContentSchema = z.object({
version: z.literal(1),
token: z.object({
id: z.string().length(INTERCEPTOR_TOKEN_ID_HEX_LENGTH).regex(HEX_REGEX),
name: z.string().optional(),
secret: z.object({
hash: z.string().length(INTERCEPTOR_TOKEN_HASH_HEX_LENGTH).regex(HEX_REGEX),
salt: z.string().length(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH).regex(HEX_REGEX)
}),
createdAt: z.iso.datetime().transform((value) => new Date(value))
})
});
async function saveInterceptorTokenToFile(tokensDirectory, token) {
const tokeFilePath = path.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.promises.writeFile(tokeFilePath, JSON.stringify(tokenFileContent, null, 2), {
mode: 384,
encoding: "utf-8"
});
return tokeFilePath;
}
__name(saveInterceptorTokenToFile, "saveInterceptorTokenToFile");
async function readInterceptorTokenFromFile(tokenId, options) {
if (!isValidInterceptorTokenId(tokenId)) {
throw new InvalidInterceptorTokenError_default(tokenId);
}
const tokenFilePath = path.join(options.tokensDirectory, tokenId);
const tokenFileExists = await pathExists(tokenFilePath);
if (!tokenFileExists) {
return null;
}
const tokenFileContentAsString = await fs.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;
}
__name(readInterceptorTokenFromFile, "readInterceptorTokenFromFile");
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.randomBytes(tokenSecretSizeInBytes).toString("hex");
const tokenSecretSaltSizeInBytes = convertHexLengthToByteLength(INTERCEPTOR_TOKEN_SALT_HEX_LENGTH);
const tokenSecretSalt = crypto.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: {