UNPKG

@zimic/interceptor

Version:

Next-gen TypeScript-first HTTP intercepting and mocking

1,425 lines (1,386 loc) 69.4 kB
'use strict'; var chunkWCQVDF3K_js = require('./chunk-WCQVDF3K.js'); 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 { static { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__name(this, "HttpServerTimeoutError"); } }; var HttpServerStartTimeoutError = class extends HttpServerTimeoutError { static { chunkWCQVDF3K_js.__name(this, "HttpServerStartTimeoutError"); } constructor(reachedTimeout) { super(`HTTP server start timed out after ${reachedTimeout}ms.`); this.name = "HttpServerStartTimeout"; } }; var HttpServerStopTimeoutError = class extends HttpServerTimeoutError { static { chunkWCQVDF3K_js.__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); } chunkWCQVDF3K_js.__name(handleStartError, "handleStartError"); const startTimeout = setTimeout(() => { const timeoutError = new HttpServerStartTimeoutError(timeoutDuration); handleStartError(timeoutError); }, timeoutDuration); function handleStartSuccess() { server.off("error", handleStartError); clearTimeout(startTimeout); resolve(); } chunkWCQVDF3K_js.__name(handleStartSuccess, "handleStartSuccess"); server.once("error", handleStartError); server.listen(port, hostname, handleStartSuccess); }); } chunkWCQVDF3K_js.__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(); }); } chunkWCQVDF3K_js.__name(stopHttpServer, "stopHttpServer"); function getHttpServerPort(server) { const address = server.address(); if (typeof address === "string") { return void 0; } else { return address?.port; } } chunkWCQVDF3K_js.__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); } chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__name(this, "UnauthorizedWebSocketConnectionError"); } }; var UnauthorizedWebSocketConnectionError_default = UnauthorizedWebSocketConnectionError; // src/utils/webSocket.ts var WebSocketTimeoutError = class extends Error { static { chunkWCQVDF3K_js.__name(this, "WebSocketTimeoutError"); } }; var WebSocketOpenTimeoutError = class extends WebSocketTimeoutError { static { chunkWCQVDF3K_js.__name(this, "WebSocketOpenTimeoutError"); } constructor(reachedTimeout) { super(`Web socket open timed out after ${reachedTimeout}ms.`); this.name = "WebSocketOpenTimeout"; } }; var WebSocketMessageTimeoutError = class extends WebSocketTimeoutError { static { chunkWCQVDF3K_js.__name(this, "WebSocketMessageTimeoutError"); } constructor(reachedTimeout) { super(`Web socket message timed out after ${reachedTimeout}ms.`); this.name = "WebSocketMessageTimeout"; } }; var WebSocketMessageAbortError = class extends WebSocketTimeoutError { static { chunkWCQVDF3K_js.__name(this, "WebSocketMessageAbortError"); } constructor() { super("Web socket message was aborted."); this.name = "WebSocketMessageAbortError"; } }; var WebSocketCloseTimeoutError = class extends WebSocketTimeoutError { static { chunkWCQVDF3K_js.__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); } chunkWCQVDF3K_js.__name(removeAllSocketListeners, "removeAllSocketListeners"); function handleOpenError(error) { removeAllSocketListeners(); reject(error); } chunkWCQVDF3K_js.__name(handleOpenError, "handleOpenError"); function handleClose(event) { const isUnauthorized = event.code === 1008; if (isUnauthorized) { const unauthorizedError = new UnauthorizedWebSocketConnectionError_default(event); handleOpenError(unauthorizedError); } else { handleOpenError(event); } } chunkWCQVDF3K_js.__name(handleClose, "handleClose"); const openTimeout = setTimeout(() => { const timeoutError = new WebSocketOpenTimeoutError(timeoutDuration); handleOpenError(timeoutError); }, timeoutDuration); function handleOpenSuccess() { removeAllSocketListeners(); clearTimeout(openTimeout); resolve(); } chunkWCQVDF3K_js.__name(handleOpenSuccess, "handleOpenSuccess"); function handleSocketMessage(message) { const hasValidAuth = message.data === "socket:auth:valid"; if (hasValidAuth) { handleOpenSuccess(); } } chunkWCQVDF3K_js.__name(handleSocketMessage, "handleSocketMessage"); if (waitForAuthentication) { socket.addEventListener("message", handleSocketMessage); } else { socket.addEventListener("open", handleOpenSuccess); } socket.addEventListener("error", handleOpenError); socket.addEventListener("close", handleClose); }); } chunkWCQVDF3K_js.__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); } chunkWCQVDF3K_js.__name(removeAllSocketListeners, "removeAllSocketListeners"); function handleError(error) { removeAllSocketListeners(); reject(error); } chunkWCQVDF3K_js.__name(handleError, "handleError"); const closeTimeout = setTimeout(() => { const timeoutError = new WebSocketCloseTimeoutError(timeoutDuration); handleError(timeoutError); }, timeoutDuration); function handleClose() { removeAllSocketListeners(); clearTimeout(closeTimeout); resolve(); } chunkWCQVDF3K_js.__name(handleClose, "handleClose"); socket.addEventListener("error", handleError); socket.addEventListener("close", handleClose); socket.close(); }); } chunkWCQVDF3K_js.__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(); } }); }); } chunkWCQVDF3K_js.__name(closeServerSocket, "closeServerSocket"); // 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-2D3UJWOA.mjs var __defProp = Object.defineProperty; var __name2 = /* @__PURE__ */ chunkWCQVDF3K_js.__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}(?:/)?$`); } chunkWCQVDF3K_js.__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; } chunkWCQVDF3K_js.__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; } chunkWCQVDF3K_js.__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; } chunkWCQVDF3K_js.__name(removeArrayIndex, "removeArrayIndex"); function removeArrayElement(array, element) { const index = array.indexOf(element); return removeArrayIndex(array, index); } chunkWCQVDF3K_js.__name(removeArrayElement, "removeArrayElement"); // src/utils/environment.ts function isClientSide() { return typeof window !== "undefined" && typeof document !== "undefined"; } chunkWCQVDF3K_js.__name(isClientSide, "isClientSide"); // ../zimic-utils/dist/import/createCachedDynamicImport.mjs function createCachedDynamicImport(importModuleDynamically) { let cachedImportResult; return /* @__PURE__ */ __name2(/* @__PURE__ */ chunkWCQVDF3K_js.__name(async function importModuleDynamicallyWithCache() { cachedImportResult ??= await importModuleDynamically(); return cachedImportResult; }, "importModuleDynamicallyWithCache"), "importModuleDynamicallyWithCache"); } chunkWCQVDF3K_js.__name(createCachedDynamicImport, "createCachedDynamicImport"); __name2(createCachedDynamicImport, "createCachedDynamicImport"); var createCachedDynamicImport_default = createCachedDynamicImport; // ../zimic-utils/dist/logging/Logger.mjs var Logger = class _Logger { static { chunkWCQVDF3K_js.__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; } } chunkWCQVDF3K_js.__name(pathExists, "pathExists"); // 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 }); } chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 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 === 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 http.HttpHeaders(rawRequest.headers); const pathParams = options.urlRegex ? this.parseRawPathParams(options.urlRegex, rawRequest) : {}; 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 this.parseRawBody(rawResponse); 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(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 http.HttpSearchParams(bodyAsText); return bodyAsSearchParams; } static async parseRawBodyAsFormData(resource) { const resourceClone = resource.clone(); try { const bodyAsRawFormData = await resource.formData(); const bodyAsFormData = new http.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__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"); } } chunkWCQVDF3K_js.__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"); } } chunkWCQVDF3K_js.__name(convertBase64ToArrayBuffer, "convertBase64ToArrayBuffer"); var HEX_REGEX = /^[a-z0-9]+$/; function convertHexLengthToByteLength(hexLength) { return Math.ceil(hexLength / 2); } chunkWCQVDF3K_js.__name(convertHexLengthToByteLength, "convertHexLengthToByteLength"); var BASE64URL_REGEX = /^[a-zA-Z0-9-_]+$/; function convertHexLengthToBase64urlLength(hexLength) { const byteLength = convertHexLengthToByteLength(hexLength); return Math.ceil(byteLength * 4 / 3); } chunkWCQVDF3K_js.__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 }; } chunkWCQVDF3K_js.__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) }); } chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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__ */ chunkWCQVDF3K_js.__name(async (rawMessage) => { await this.handleSocketMessage(socket, rawMessage); }, "handleSocketMessage"); socket.addEventListener("message", handleSocketMessage); await openPromise; function handleSocketError(error) { console.error(error); } chunkWCQVDF3K_js.__name(handleSocketError, "handleSocketError"); socket.addEventListener("error", handleSocketError); const handleSocketClose = /* @__PURE__ */ chunkWCQVDF3K_js.__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__ */ chunkWCQVDF3K_js.__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__default.default; var WebSocketServer = class extends WebSocketHandler_default { static { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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 { chunkWCQVDF3K_js.__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__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; } chunkWCQVDF3K_js.__name(hashInterceptorToken, "hashInterceptorToken"); function createInterceptorTokenId() { return crypto__default.default.randomUUID().replace(/[^a-z0-9]/g, ""); } chunkWCQVDF3K_js.__name(createInterceptorTokenId, "createInterceptorTokenId"); function isValidInterceptorTokenId(tokenId) { return tokenId.length === INTERCEPTOR_TOKEN_ID_HEX_LENGTH && HEX_REGEX.test(tokenId); } chunkWCQVDF3K_js.__name(isValidInterceptorTokenId, "isValidInterceptorTokenId"); function isValidInterceptorTokenValue(tokenValue) { return tokenValue.length === INTERCEPTOR_TOKEN_VALUE_BASE64URL_LENGTH && BASE64URL_REGEX.test(tokenValue); } chunkWCQVDF3K_js.__name(isValidInterceptorTokenValue, "isValidInterceptorTokenValue"); 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; } } chunkWCQVDF3K_js.__name(createInterceptorTokensDirectory, "createInterceptorTokensDirectory"); 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,