UNPKG

@zimic/interceptor

Version:

Next-gen TypeScript-first HTTP intercepting and mocking

1,410 lines (1,374 loc) 65.3 kB
'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