From 19a702329185a6aac037fe3304c944bad9c6e5c4 Mon Sep 17 00:00:00 2001 From: boa604 Date: Mon, 2 Feb 2026 17:53:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D1=81=D0=B8/tls=20=D0=B2=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D1=83=D0=BB=D0=B5=20http=5Fclient,=20=D1=82=D0=B5=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D1=8C=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7?= =?UTF-8?q?=D1=83=D1=8E=D1=82=D1=81=D1=8F=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D0=B8=20http,=20https=20=D0=B8=20net=20=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=20undici.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20thick-=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D0=B0=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B5=20=D1=81=20=D0=B1=D0=B0=D0=B7=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20Oracle=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20"=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=20=D0=93=D0=90=D0=A0".=20=D0=98?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE?= =?UTF-8?q?=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B2=D1=85=D0=BE=D0=B4=D1=8F=D1=89=D0=B8=D1=85=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20(in=5Fqueue)?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/http_client.js | 544 +++++++++++++++++++++++---- core/in_queue.js | 62 +-- core/out_queue_processor.js | 6 +- core/service_available_controller.js | 2 +- modules/diadoc.js | 53 ++- modules/gar_utils/import.js | 18 + modules/sbis.js | 2 +- package-lock.json | 96 +++-- package.json | 1 - 9 files changed, 604 insertions(+), 180 deletions(-) diff --git a/core/http_client.js b/core/http_client.js index acc1825..d690a38 100644 --- a/core/http_client.js +++ b/core/http_client.js @@ -7,7 +7,10 @@ // Подключение внешних библиотек //------------------------------ +const http = require("http"); +const https = require("https"); const { URL } = require("url"); +const { Socket } = require("net"); //-------------------------- // Локальные идентификаторы @@ -15,6 +18,362 @@ const { URL } = require("url"); const DEFAULT_TIMEOUT = 30000; //Таймаут по умолчанию +//Ошибка HTTP-запроса +class HttpError extends Error { + constructor(message, response) { + super(message); + this.name = "HttpError"; + this.response = response; + } +} + +//Обработка ответа (парсинг JSON, форматирование для совместимости с request-promise) +const processResponse = (result, options) => { + let processedBody = result.body; + + //Если запрошен автоматический парсинг JSON + if (options.json === true && processedBody) { + try { + const text = processedBody.toString(options.encoding || "utf8"); + processedBody = text ? JSON.parse(text) : null; + } catch (e) { + //Если не удалось распарсить JSON, возвращаем как есть + processedBody = processedBody.toString(options.encoding || "utf8"); + } + } else if (options.encoding && processedBody) { + //Если указана кодировка, конвертируем в строку + processedBody = processedBody.toString(options.encoding); + } + + //Формируем результат в зависимости от resolveWithFullResponse + if (options.resolveWithFullResponse) { + return { + statusCode: result.statusCode, + statusMessage: result.statusMessage || "", + headers: result.headers, + body: processedBody, + url: result.url + }; + } else { + //Если json: true, возвращаем распарсенный объект, иначе Buffer/string + return processedBody; + } +}; + +//Выполнение HTTP/HTTPS запроса через встроенные модули (для прокси и TLS) +const httpRequestNative = (options, url, headers, body) => { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === "https:"; + const httpModule = isHttps ? https : http; + + //Настройки TLS + const tlsOptions = {}; + if (options.ca) { + tlsOptions.ca = Array.isArray(options.ca) ? options.ca : [options.ca]; + } + if (options.cert) { + tlsOptions.cert = options.cert; + } + if (options.key) { + tlsOptions.key = options.key; + } + if (options.passphrase) { + tlsOptions.passphrase = options.passphrase; + } + if (options.rejectUnauthorized !== undefined) { + tlsOptions.rejectUnauthorized = options.rejectUnauthorized; + } + + //Настройки прокси + let proxyUrl = null; + let proxyAuth = null; + if (options.proxy) { + try { + proxyUrl = new URL(options.proxy); + if (proxyUrl.username || proxyUrl.password) { + proxyAuth = `Basic ${Buffer.from(`${decodeURIComponent(proxyUrl.username || "")}:${decodeURIComponent(proxyUrl.password || "")}`).toString("base64")}`; + } + } catch (e) { + return reject(new Error(`Некорректный URL прокси: ${e.message}`)); + } + } + + //Параметры запроса + const requestOptions = { + hostname: proxyUrl ? proxyUrl.hostname : urlObj.hostname, + port: proxyUrl ? proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80) : urlObj.port || (isHttps ? 443 : 80), + path: proxyUrl ? url : urlObj.pathname + urlObj.search, + method: options.method, + headers: Object.fromEntries(headers), + timeout: options.timeout || DEFAULT_TIMEOUT + }; + + //Добавляем авторизацию прокси в заголовки + if (proxyAuth) { + requestOptions.headers["Proxy-Authorization"] = proxyAuth; + } + + //Для HTTPS добавляем TLS опции + if (isHttps && Object.keys(tlsOptions).length > 0) { + Object.assign(requestOptions, tlsOptions); + } + + //Если используется прокси, нужно использовать CONNECT метод для HTTPS + if (proxyUrl && isHttps) { + //Для HTTPS через прокси используем туннелирование + return httpRequestThroughProxy(requestOptions, urlObj, headers, body, proxyUrl, proxyAuth, tlsOptions, options) + .then(result => resolve(processResponse(result, options))) + .catch(reject); + } + + //Обычный запрос (с прокси для HTTP или без прокси) + const req = httpModule.request(requestOptions, res => { + const chunks = []; + res.on("data", chunk => { + chunks.push(chunk); + }); + res.on("end", () => { + const responseBody = Buffer.concat(chunks); + const result = { + statusCode: res.statusCode, + statusMessage: res.statusMessage || "", + headers: res.headers, + body: responseBody, + ok: res.statusCode >= 200 && res.statusCode < 300, + url: url + }; + + if (options.throwOnErrorStatus && !result.ok) { + const error = new HttpError(`Запрос не выполнен со статусом ${result.statusCode}`, result); + const httpError = new Error(error.message); + httpError.response = result; + httpError.statusCode = result.statusCode; + return reject(httpError); + } + + resolve(processResponse(result, options)); + }); + }); + + req.on("error", err => { + const error = new Error(err.message); + error.code = err.code; + error.error = { code: err.code }; + reject(error); + }); + + req.on("timeout", () => { + req.destroy(); + const timeoutError = new Error(`Время ожидания выполнения запроса истекло после ${options.timeout || DEFAULT_TIMEOUT} мс`); + timeoutError.code = "ETIMEDOUT"; + timeoutError.error = { code: "ETIMEDOUT" }; + reject(timeoutError); + }); + + //Отправляем тело запроса + if (body) { + if (Buffer.isBuffer(body)) { + req.write(body); + } else if (typeof body === "string") { + req.write(Buffer.from(body)); + } else { + req.write(Buffer.from(JSON.stringify(body))); + } + } + + req.end(); + }); +}; + +//Выполнение HTTPS запроса через HTTP прокси (туннелирование) +const httpRequestThroughProxy = (requestOptions, targetUrl, headers, body, proxyUrl, proxyAuth, tlsOptions, options) => { + const timeout = options.timeout || DEFAULT_TIMEOUT; + return new Promise((resolve, reject) => { + const proxyHost = proxyUrl.hostname; + const proxyPort = parseInt(proxyUrl.port || (proxyUrl.protocol === "https:" ? 443 : 80), 10); + const targetHost = targetUrl.hostname; + const targetPort = parseInt(targetUrl.port || 443, 10); + + //Создаем соединение с прокси + const proxySocket = new Socket(); + let connected = false; + let requestSent = false; + + const connectTimeout = setTimeout(() => { + if (!connected || !requestSent) { + proxySocket.destroy(); + reject(new Error(`Время ожидания подключения к прокси истекло после ${timeout} мс`)); + } + }, timeout); + + proxySocket.setTimeout(timeout); + proxySocket.on("timeout", () => { + proxySocket.destroy(); + reject(new Error(`Время ожидания выполнения запроса истекло после ${timeout} мс`)); + }); + + proxySocket.connect(proxyPort, proxyHost, () => { + connected = true; + + //Отправляем CONNECT запрос + const connectRequest = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`; + const authHeader = proxyAuth ? `Proxy-Authorization: ${proxyAuth}\r\n` : ""; + const connectHeaders = connectRequest + authHeader + "\r\n"; + + proxySocket.write(connectHeaders); + + let connectResponse = ""; + const onConnectData = chunk => { + connectResponse += chunk.toString(); + const headerEnd = connectResponse.indexOf("\r\n\r\n"); + if (headerEnd !== -1) { + proxySocket.removeListener("data", onConnectData); + clearTimeout(connectTimeout); + + //Проверяем ответ на CONNECT + const statusLine = connectResponse.substring(0, connectResponse.indexOf("\r\n")); + if (statusLine.includes("200")) { + //Успешное подключение, создаем TLS соединение + const tls = require("tls"); + const secureSocket = tls.connect( + { + socket: proxySocket, + host: targetHost, + servername: targetHost, + ...tlsOptions + }, + () => { + requestSent = true; + clearTimeout(connectTimeout); + + //Отправляем HTTP запрос через TLS + const path = targetUrl.pathname + targetUrl.search; + const requestLine = `${requestOptions.method} ${path} HTTP/1.1\r\n`; + const hostHeader = `Host: ${targetHost}${targetPort !== 443 ? `:${targetPort}` : ""}\r\n`; + const requestHeaders = Array.from(headers.entries()) + .map(([key, value]) => `${key}: ${value}\r\n`) + .join(""); + const httpRequest = requestLine + hostHeader + requestHeaders + "\r\n"; + + secureSocket.write(httpRequest); + + if (body) { + const bodyBuffer = Buffer.isBuffer(body) + ? body + : typeof body === "string" + ? Buffer.from(body) + : Buffer.from(JSON.stringify(body)); + secureSocket.write(bodyBuffer); + } + + //Читаем ответ + let responseBuffer = Buffer.alloc(0); + let responseHeaders = {}; + let statusCode = 200; + let headerParsed = false; + let contentLength = -1; + + const onSecureData = chunk => { + responseBuffer = Buffer.concat([responseBuffer, chunk]); + + if (!headerParsed) { + const headerEnd = responseBuffer.indexOf("\r\n\r\n"); + if (headerEnd !== -1) { + const headerText = responseBuffer.subarray(0, headerEnd).toString(); + const bodyStart = headerEnd + 4; + responseBuffer = responseBuffer.subarray(bodyStart); + + //Парсим заголовки + const lines = headerText.split("\r\n"); + const statusLine = lines[0]; + const statusMatch = statusLine.match(/HTTP\/[\d.]+\s+(\d+)/); + if (statusMatch) { + statusCode = parseInt(statusMatch[1], 10); + } + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + const colonIndex = line.indexOf(":"); + if (colonIndex !== -1) { + const key = line.substring(0, colonIndex).trim().toLowerCase(); + const value = line.substring(colonIndex + 1).trim(); + responseHeaders[key] = value; + if (key === "content-length") { + contentLength = parseInt(value, 10); + } + } + } + + headerParsed = true; + + //Если есть Content-Length и мы уже получили все данные + if (contentLength >= 0 && responseBuffer.length >= contentLength) { + secureSocket.removeListener("data", onSecureData); + secureSocket.end(); + } + } + } else { + //Проверяем, получили ли мы все данные + if (contentLength >= 0 && responseBuffer.length >= contentLength) { + secureSocket.removeListener("data", onSecureData); + secureSocket.end(); + } else if (contentLength < 0) { + //Нет Content-Length, читаем до конца + //Будем ждать закрытия соединения + } + } + }; + + secureSocket.on("data", onSecureData); + + secureSocket.on("end", () => { + const result = { + statusCode: statusCode, + statusMessage: "", + headers: responseHeaders, + body: responseBuffer, + ok: statusCode >= 200 && statusCode < 300, + url: targetUrl.toString() + }; + + if (options.throwOnErrorStatus && !result.ok) { + const error = new HttpError(`Запрос не выполнен со статусом ${result.statusCode}`, result); + const httpError = new Error(error.message); + httpError.response = result; + httpError.statusCode = result.statusCode; + return reject(httpError); + } + + resolve(processResponse(result, options)); + }); + + secureSocket.on("error", err => { + reject(err); + }); + } + ); + + secureSocket.on("error", err => { + reject(err); + }); + } else { + proxySocket.destroy(); + reject(new Error(`Прокси вернул ошибку: ${statusLine}`)); + } + } + }; + + proxySocket.on("data", onConnectData); + }); + + proxySocket.on("error", err => { + clearTimeout(connectTimeout); + reject(err); + }); + }); +}; + //Выполнение HTTP/HTTPS запроса const httpRequest = async (rawOptions = {}) => { try { @@ -26,7 +385,7 @@ const httpRequest = async (rawOptions = {}) => { const headers = prepareHeaders(options.headers); const { body, contentLength, isStream } = prepareBody({ body: options.body, - json: options.json, + jsonRequest: options.jsonRequest, headers }); //Если не указан размер тела @@ -34,30 +393,8 @@ const httpRequest = async (rawOptions = {}) => { //Установим размер тела в заголовок headers.set("content-length", String(contentLength)); } - //Выполним запрос с использованием fetch - return httpRequestFetch({ options, url, headers, body, isStream }); - } catch (e) { - throw e; - } -}; - -//Ошибка HTTP-запроса -class HttpError extends Error { - constructor(message, response) { - super(message); - this.name = "HttpError"; - this.response = response; - } -} - -//Выполнение запроса с использованием fetch -const httpRequestFetch = async ({ options, url, headers, body, isStream }) => { - try { - const fetchImpl = globalThis.fetch; - if (typeof fetchImpl !== "function") { - throw new Error("globalThis.fetch недоступен. Используйте Node.js >= 18."); - } + //Если есть поток, читаем его let requestBody = body; if (isStream && body && typeof body.pipe === "function") { const chunks = []; @@ -71,6 +408,17 @@ const httpRequestFetch = async ({ options, url, headers, body, isStream }) => { } } + //Если нужен прокси или специальные TLS настройки, используем встроенные модули + if (options.proxy || options.ca || options.cert || options.key || options.passphrase || options.rejectUnauthorized === false) { + return await httpRequestNative(options, url.toString(), headers, requestBody); + } + + //Для простых запросов используем встроенный fetch + const fetchImpl = globalThis.fetch; + if (typeof fetchImpl !== "function") { + throw new Error("globalThis.fetch недоступен. Используйте Node.js >= 18."); + } + const timeoutController = new AbortController(); const signals = []; @@ -97,53 +445,11 @@ const httpRequestFetch = async ({ options, url, headers, body, isStream }) => { signal: combinedSignal, redirect: options.followRedirects ? "follow" : "manual" }; - - if (options.proxy || options.ca || options.cert || options.key || options.passphrase || options.rejectUnauthorized === false) { - const { Agent, ProxyAgent } = require("undici"); - const connectOptions = { - rejectUnauthorized: options.rejectUnauthorized !== undefined ? options.rejectUnauthorized : true - }; - - if (options.ca) { - connectOptions.ca = Array.isArray(options.ca) ? options.ca : [options.ca]; - } - if (options.cert) { - connectOptions.cert = options.cert; - } - if (options.key) { - connectOptions.key = options.key; - } - if (options.passphrase) { - connectOptions.passphrase = options.passphrase; - } - - if (options.proxy) { - const proxyUrl = new URL(options.proxy); - if (proxyUrl.protocol !== "http:" && proxyUrl.protocol !== "https:") { - throw new Error("Поддерживаются только HTTP/HTTPS-прокси (protocol=http|https)."); - } - const proxyAuth = - proxyUrl.username || proxyUrl.password - ? `Basic ${Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString( - "base64" - )}` - : null; - fetchOptions.dispatcher = new ProxyAgent({ - uri: options.proxy, - token: proxyAuth, - connect: connectOptions - }); - } else { - fetchOptions.dispatcher = new Agent({ - connect: connectOptions - }); - } - } - const response = await fetchImpl(url.toString(), fetchOptions); const responseBody = Buffer.from(await response.arrayBuffer()); const result = { statusCode: response.status, + statusMessage: response.statusText || "", headers: Object.fromEntries(response.headers.entries()), body: responseBody, ok: response.ok, @@ -151,14 +457,39 @@ const httpRequestFetch = async ({ options, url, headers, body, isStream }) => { }; if (options.throwOnErrorStatus && !result.ok) { - throw new HttpError(`Запрос не выполнен со статусом ${result.statusCode}`, result); + const error = new HttpError(`Запрос не выполнен со статусом ${result.statusCode}`, result); + const httpError = new Error(error.message); + httpError.response = result; + httpError.statusCode = result.statusCode; + throw httpError; } - return result; + return processResponse(result, options); } finally { if (timeoutId) clearTimeout(timeoutId); } } catch (e) { + //Преобразуем ошибки для совместимости с request-promise + if (e.name === "AbortError" || e.message.includes("время ожидания")) { + const timeoutError = new Error(e.message); + timeoutError.code = "ETIMEDOUT"; + timeoutError.error = { code: "ETIMEDOUT" }; + throw timeoutError; + } + //Если ошибка уже имеет response (из httpRequestNative или fetch), пробрасываем как есть + if (e.response) { + throw e; + } + if (e instanceof HttpError) { + const httpError = new Error(e.message); + httpError.response = e.response; + httpError.statusCode = e.response.statusCode; + throw httpError; + } + //Для других ошибок добавляем поле error для совместимости + if (e.code) { + e.error = { code: e.code }; + } throw e; } }; @@ -168,24 +499,58 @@ const normalizeOptions = options => { if (!options || typeof options !== "object") { throw new TypeError("options должен быть объектом"); } - if (!options.url) throw new Error("options.url обязателен"); + + //Поддержка параметра uri (как в request-promise) + const url = options.uri || options.url; + if (!url) throw new Error("options.url или options.uri обязателен"); + + //Обработаем JSON + const hasJsonOption = Object.prototype.hasOwnProperty.call(options, "json"); + const jsonOption = options.json; + + let body = options.body; + let jsonRequest = false; + let json = false; + + if (hasJsonOption) { + if (jsonOption === true) { + json = true; + } else if (jsonOption && typeof jsonOption === "object") { + if (body === undefined) { + body = jsonOption; + jsonRequest = true; + } + json = true; + } + } + + //Определяем HTTP-метод + let method = options.method; + if (!method) { + const hasRequestBody = body != null; + method = hasRequestBody ? "POST" : "GET"; + } + return { - method: (options.method || "GET").toUpperCase(), - url: options.url, + method: String(method).toUpperCase(), + url, headers: options.headers || {}, query: options.query || options.qs || {}, - body: options.body, - json: options.json, + body, + json, + jsonRequest, timeout: options.timeout ?? DEFAULT_TIMEOUT, - followRedirects: options.followRedirects ?? false, + followRedirects: options.followRedirects ?? true, proxy: options.proxy || null, ca: options.ca, cert: options.cert, key: options.key, passphrase: options.passphrase, rejectUnauthorized: options.rejectUnauthorized !== undefined ? options.rejectUnauthorized : true, - throwOnErrorStatus: options.throwOnErrorStatus ?? false, - signal: options.signal || null + throwOnErrorStatus: options.throwOnErrorStatus ?? options.simple !== false, + signal: options.signal || null, + resolveWithFullResponse: options.resolveWithFullResponse || false, + encoding: options.encoding || null }; }; @@ -210,13 +575,18 @@ const prepareHeaders = (inputHeaders = {}) => { }; //Подготовка тела запроса -const prepareBody = ({ body, json, headers }) => { - if (json !== undefined) { - const payload = Buffer.from(JSON.stringify(json)); +const prepareBody = ({ body, jsonRequest, headers }) => { + if (body === undefined || body === null) { + return { body: undefined, contentLength: undefined, isStream: false }; + } + + //Явно указано, что тело нужно сериализовать как JSON + if (jsonRequest) { + const payload = Buffer.from(JSON.stringify(body)); if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8"); return { body: payload, contentLength: payload.length, isStream: false }; } - if (body === undefined || body === null) return { body: undefined, contentLength: undefined, isStream: false }; + if (Buffer.isBuffer(body) || body instanceof Uint8Array) { return { body, contentLength: body.length, isStream: false }; } @@ -227,8 +597,16 @@ const prepareBody = ({ body, json, headers }) => { if (typeof body.pipe === "function") { return { body, contentLength: undefined, isStream: true }; } - const payload = Buffer.from(JSON.stringify(body)); - if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8"); + + //По умолчанию объектное тело сериализуем в JSON + if (typeof body === "object") { + const payload = Buffer.from(JSON.stringify(body)); + if (!headers.has("content-type")) headers.set("content-type", "application/json; charset=utf-8"); + return { body: payload, contentLength: payload.length, isStream: false }; + } + + //Фолбэк для прочих типов + const payload = Buffer.from(String(body)); return { body: payload, contentLength: payload.length, isStream: false }; }; diff --git a/core/in_queue.js b/core/in_queue.js index 379e46a..5962119 100644 --- a/core/in_queue.js +++ b/core/in_queue.js @@ -123,13 +123,25 @@ class InQueue extends EventEmitter { let optionsResp = {}; //Флаг прекращения обработки сообщения let bStopPropagation = false; + //Нормализация сообщения + const toMessageBuffer = body => { + if (body === undefined || body === null) return null; + if (Buffer.isBuffer(body)) return body.length ? body : null; + if (body instanceof Uint8Array) return body.length ? Buffer.from(body) : null; + if (typeof body === "string") return body.length ? Buffer.from(body, "utf8") : null; + if (typeof body === "object") { + if (Object.keys(body).length === 0) return null; + return Buffer.from(JSON.stringify(body)); + } + return Buffer.from(String(body)); + }; //Определимся с телом сообщения - для POST, PATCH и PUT сообщений - это тело запроса if ( [objServiceFnSchema.NFN_PRMS_TYPE_POST, objServiceFnSchema.NFN_PRMS_TYPE_PATCH, objServiceFnSchema.NFN_PRMS_TYPE_PUT].includes( prms.function.nFnPrmsType ) ) { - blMsg = prms.req.body && !(Object.keys(prms.req.body ?? {}).length === 0) ? Buffer.from(JSON.stringify(prms.req.body)) : null; + blMsg = toMessageBuffer(prms.req.body); } else { //Для GET, HEAD, DELETE, CONNECT, OPTIONS и TRACE - параметры запроса if (!(Object.keys(prms.req.query ?? {}).length === 0)) blMsg = Buffer.from(JSON.stringify(prms.req.query)); @@ -151,6 +163,8 @@ class InQueue extends EventEmitter { sOptions: buildOptionsXML({ options }), blMsg }); + //Запомним идентификатор записи очереди в запросе + prms.req.nQId = q.nId; //Скажем что пришло новое входящее сообщение await this.logger.info( `Новое входящее сообщение от ${prms.req.connection.address().address} для функции ${prms.function.sCode} (${buildURL({ @@ -328,13 +342,19 @@ class InQueue extends EventEmitter { } } } - //Если мы еще не отдали ответ от сервера - if (!prms.res.writableFinished) { - //Всё успешно - отдаём результат клиенту - if (bStopPropagation === false) { - if (optionsResp.headers) prms.res.set(optionsResp.headers); - prms.res.status(optionsResp.statusCode || 200).send(blResp); - } + //Всё успешно - отдаём результат клиенту, если ещё не отдали + if (bStopPropagation === false && !prms.res.writableFinished) { + if (optionsResp.headers) prms.res.set(optionsResp.headers); + prms.res.status(optionsResp.statusCode || 200).send(blResp); + } + //Если отправка ответа была прервана по таймауту + if (prms.req.bIsTimedOut === true) { + //Вернем ошибку обработчика с информацией об этом + throw new ServerError( + SERR_WEB_SERVER, + "Истекло время ожидания обработки входящего запроса. Канал закрыт. Клиенту был отправлен ответ с ошибкой истечения таймаута (504)." + ); + } else { //Фиксируем успех обработки - в протоколе работы сервиса await this.logger.info(`Входящее сообщение ${q.nId} успешно отработано`, { nQueueId: q.nId }); //Фиксируем успех обработки - в статусе сообщения @@ -343,12 +363,6 @@ class InQueue extends EventEmitter { nIncExecCnt: NINC_EXEC_CNT_YES, nExecState: objQueueSchema.NQUEUE_EXEC_STATE_OK }); - } else { - //Или расскажем об ошибке - throw new ServerError( - SERR_WEB_SERVER, - "Истекло время ожидания обработки входящего запроса. Канал закрыт. Клиенту был отправлен ответ." - ); } } catch (e) { //Тема и текст уведомления об ошибке @@ -554,21 +568,24 @@ class InQueue extends EventEmitter { if (req.headers["content-type"] === "false") req.headers["content-type"] = "application/octet-stream"; next(); }); - //Если требуется установить таймаут на обработку сообщений - if (this.inComing.nTimeout !== 0) { - //Конфигурируем сервер - устанавливаем таймаут обработки сообщений - this.webApp.use((req, res, next) => { + //Конфигурируем сервер - устанавливаем таймаут обработки сообщений + this.webApp.use((req, res, next) => { + //Поднимем флаг истечения таймаута обработки + req.bIsTimedOut = false; + //Если требуется установить таймаут на обработку сообщений + if (this.inComing.nTimeout !== 0) //Устанавливаем таймаут на ответ от сервера res.setTimeout(this.inComing.nTimeout, () => { + //Поднимем флаг исчетечение таймаута обработки + req.bIsTimedOut = true; //Формируем ошибку let err = new Error("Истекло время ожидания формирования ответа для завершения текущего запроса."); err.status = 504; //Отправляем ошибку next(err); }); - next(); - }); - } + next(); + }); //Конфигурируем сервер - обработка тела сообщения this.webApp.use(express.json()); this.webApp.use(express.urlencoded({ extended: true })); @@ -624,7 +641,8 @@ class InQueue extends EventEmitter { //Протоколируем в журнал работы сервера await this.logger.error(makeErrorText(new ServerError(SERR_WEB_SERVER, err.message)), { nServiceId: srvs.nId, - nServiceFnId: fn.nId + nServiceFnId: fn.nId, + nQueueId: req.nQId || null }); //Отправим ошибку клиенту res.status(err.status || 500).send(makeErrorText(new ServerError(SERR_WEB_SERVER, err.message))); diff --git a/core/out_queue_processor.js b/core/out_queue_processor.js index 9c80c39..e2e5de0 100644 --- a/core/out_queue_processor.js +++ b/core/out_queue_processor.js @@ -342,7 +342,8 @@ const appProcess = async prms => { url: options.url, auth: options.auth, topic: options.topic, - message: options.body + message: options.body, + logger }); break; //MQTT/MQTTS @@ -352,7 +353,8 @@ const appProcess = async prms => { url: options.url, auth: options.auth, topic: options.topic, - message: options.body + message: options.body, + logger }); break; //HTTP/HTTPS diff --git a/core/service_available_controller.js b/core/service_available_controller.js index 8232050..86c31c1 100644 --- a/core/service_available_controller.js +++ b/core/service_available_controller.js @@ -7,7 +7,7 @@ // Подключение внешних библиотек //------------------------------ -const httpRequest = require("./http_client"); //Работа с HTTP/HTTPS запросами +const { httpRequest } = require("./http_client"); //Работа с HTTP/HTTPS запросами const EventEmitter = require("events"); //Обработчик пользовательских событий const { ServerError } = require("./server_errors"); //Типовая ошибка const { SERR_SERVICE_UNAVAILABLE, SERR_OBJECT_BAD_INTERFACE } = require("./constants"); //Общесистемные константы diff --git a/modules/diadoc.js b/modules/diadoc.js index fd72d84..cff8552 100644 --- a/modules/diadoc.js +++ b/modules/diadoc.js @@ -8,7 +8,7 @@ //------------------------------ const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML -const httpRequest = require("../core/http_client"); //Работа с HTTP/HTTPS запросами +const { httpRequest } = require("../core/http_client"); //Работа с HTTP/HTTPS запросами const config = require("../config"); //Параметры сервера const { SDDAUTH_API_CLIENT_ID, SDEPARTMENT_NAME, SDEPARTMENT_ID } = require("./diadoc_config"); //Ключ разработчика @@ -135,15 +135,23 @@ const buildHeaders = (sAPIClientId, sToken = null) => ({ }); //Отбор организций -const getOrganizations = organizations => { +const getOrganizations = (organizations, sNumEdo = null) => { //Параметры отбора let isRoaming = false; let isActive = true; //Итоговая выборка let organization = { Organizations: [] }; - //Найдем активную организацию не в роуминге - organization.Organizations[0] = organizations.Organizations.find(org => org.IsRoaming === isRoaming && org.IsActive === isActive); - //Если не удалось получить организацию не в роуминге + //Задан код участника ЭДО + if (sNumEdo) { + //Найдем активную организацию не в роуминге + organization.Organizations[0] = organizations.Organizations.find(org => org.FnsParticipantId === sNumEdo); + } + //Не удалось получить организацию по коду участника ЭДО + if (!organization.Organizations[0]) { + //Найдем активную организацию не в роуминге + organization.Organizations[0] = organizations.Organizations.find(org => org.IsRoaming === isRoaming && org.IsActive === isActive); + } + //Не удалось получить организацию не в роуминге if (!organization.Organizations[0]) { //Найдем активную организацию organization.Organizations[0] = organizations.Organizations.find(org => org.IsActive === isActive); @@ -184,7 +192,7 @@ const getOrganization = async (sSrvRoot, headers, nInn, nKpp) => { }; //Получение ящика организации по ИНН/КПП контрагента -const getOrganizationBoxId = async (sSrvRoot, headers, nInn, nKpp) => { +const getOrganizationBoxId = async (sSrvRoot, headers, nInn, nKpp, sNumEdo) => { //Параметры запроса let httpRequestOptions; let serverResp; @@ -204,9 +212,11 @@ const getOrganizationBoxId = async (sSrvRoot, headers, nInn, nKpp) => { serverResp = await httpRequest(httpRequestOptions); try { //Получим организацию не в роуминге (или единственную организацию в роуминге) - serverResp = getOrganizations(serverResp); + serverResp = getOrganizations(serverResp, sNumEdo); if (!serverResp?.Organizations[0]) { - throw Error(`Не удалось получить ящик получателя для контрагента с ИНН: ${nInn} и КПП: ${nKpp}`); + throw Error( + `Не удалось получить ящик получателя для контрагента с ${sNumEdo ? `кодом участника ЭДО: ${sNumEdo}, ` : ""}ИНН: ${nInn} и КПП: ${nKpp}` + ); } } catch (e) { //Получим головную организацию по ИНН/КПП @@ -297,6 +307,8 @@ const beforeMessagePost = async prms => { } //Если не достали из контекста токен доступа - значит нет аутентификации на сервере if (!sToken) return { bUnAuth: true }; + //Получим параметры запроса + const optionsData = await toJSON(prms.queue.sOptions); //Конвертируем XML из "Парус 8" в JSON let obj = await toJSON(prms.queue.blMsg.toString()); //Формируем запрос для получения FromBoxId @@ -314,7 +326,7 @@ const beforeMessagePost = async prms => { //Получим идентификатор организации по ИНН/КПП поставщика документа for (let i in serverResp.Organizations) { //Если найдена подходящая организация - запомним идентификатор и выходим из цикла - if (serverResp.Organizations[i].Inn == prms.options.inn_pr && serverResp.Organizations[i].Kpp == prms.options.kpp_pr) { + if (serverResp.Organizations[i].Inn == optionsData.inn_pr && serverResp.Organizations[i].Kpp == optionsData.kpp_pr) { //Сохраняем полученный ответ obj.FromBoxId = serverResp.Organizations[i].Boxes[0].BoxId; break; @@ -322,7 +334,7 @@ const beforeMessagePost = async prms => { } //Не удалось получить ящик отправителя if (!obj.FromBoxId) { - throw new Error(`Не удалось получить ящик текущей организации с ИНН: ${prms.options.inn_pr} и КПП: ${prms.options.kpp_pr}`); + throw new Error(`Не удалось получить ящик текущей организации с ИНН: ${optionsData.inn_pr} и КПП: ${optionsData.kpp_pr}`); } } catch (e) { throw Error(`Ошибка при получении ящика текущей организации: ${e.message}`); @@ -331,8 +343,9 @@ const beforeMessagePost = async prms => { organization = await getOrganizationBoxId( prms.service.sSrvRoot, buildHeaders(sAPIClientId, sToken), - prms.options.inn_cs, - prms.options.kpp_cs + optionsData.inn_cs, + optionsData.kpp_cs, + optionsData.num_cs ); obj.ToBoxId = organization.BoxId; //Если не заполнен идентификатор подразделения и при получении ящика удалось его подобрать @@ -460,6 +473,8 @@ const beforeEvent = async prms => { } //Если не достали из контекста токен доступа - значит нет аутентификации на сервере if (!sToken) return { bUnAuth: true }; + //Получим параметры запроса + const optionsData = await toJSON(prms.queue.sOptions); //Формируем запрос для получения BoxId let httpRequestOptions = { uri: buildMyOrganizationURL(prms.service.sSrvRoot), @@ -472,7 +487,7 @@ const beforeEvent = async prms => { //Получим идентификатор организации по ИНН/КПП контрагента организации for (let i in serverResp.Organizations) { //Если найдена подходящая организация - запомним идентификатор и выходим из цикла - if (serverResp.Organizations[i].Inn == prms.options.inn && serverResp.Organizations[i].Kpp == prms.options.kpp) { + if (serverResp.Organizations[i].Inn == optionsData.inn && serverResp.Organizations[i].Kpp == optionsData.kpp) { //Сохраняем полученный ответ sBoxId = serverResp.Organizations[i].Boxes[0].BoxId; //Если задано подразделение @@ -499,7 +514,7 @@ const beforeEvent = async prms => { } //Не удалось получить ящик текущей организации if (!sBoxId) { - throw new Error(`Не удалось получить ящик текущей организации с ИНН: ${prms.options.inn} и КПП: ${prms.options.kpp}`); + throw new Error(`Не удалось получить ящик текущей организации с ИНН: ${optionsData.inn} и КПП: ${optionsData.kpp}`); } } catch (e) { throw Error(`Ошибка при получении ящика текущей организации: ${e.message}`); @@ -839,14 +854,18 @@ const beforeDepartmentIdGet = async prms => { const afterDepartmentIdGet = async prms => { let resu = null; let organization = {}; + //Получим параметры запроса + const optionsData = await toJSON(prms.queue.sOptions); //Действие выполнено успешно if (prms.optionsResp.statusCode == 200) { try { try { //Получим организацию не в роуминге (или единственную организацию в роуминге) - organization = getOrganizations(JSON.parse(prms.queue.blResp.toString())); + organization = getOrganizations(JSON.parse(prms.queue.blResp.toString()), optionsData.sNumEdo); if (!organization) { - throw Error(`Не удалось получить ящик для контрагента с ИНН: ${prms.options.nINN} и КПП: ${prms.options.nKPP}`); + throw Error( + `Не удалось получить ящик для контрагента с ${optionsData.sNumEdo ? `кодом участника ЭДО: ${optionsData.sNumEdo}, ` : ""}ИНН: ${optionsData.nINN} и КПП: ${optionsData.nKPP}` + ); } } catch (e) { //Получим ключ разработчика @@ -854,7 +873,7 @@ const afterDepartmentIdGet = async prms => { //Считаем токен доступа из контекста сервиса let sToken = prms.service.sCtx; //Получим головную организацию по ИНН/КПП - organization = await getOrganization(prms.service.sSrvRoot, buildHeaders(sAPIClientId, sToken), prms.options.nINN, prms.options.nKPP); + organization = await getOrganization(prms.service.sSrvRoot, buildHeaders(sAPIClientId, sToken), optionsData.nINN, optionsData.nKPP); } //Преобразуем JSON ответ сервиса "ДИАДОК" в XML, понятный "Парус 8" resu = toXML({ root: organization }); diff --git a/modules/gar_utils/import.js b/modules/gar_utils/import.js index 97ca921..392af0b 100644 --- a/modules/gar_utils/import.js +++ b/modules/gar_utils/import.js @@ -14,6 +14,24 @@ const { WRK_MSG_TYPE, logInf, logErr, makeTaskOKResult, makeTaskErrResult, makeS const { PARSERS, findModelByFileName } = require("./parsers"); //Модели и парсеры const sax = require("./node_modules/sax"); //Событийный XML-парсер +//--------------------------- +// Инициализация Thick-режима +//--------------------------- + +//Инициализируем Thick-режим до любых подключений к БД +try { + if (typeof oracledb.initOracleClient === "function" && !(process.env.NODE_ORACLE_DB_THIN_MODE === 1)) { + const libDir = process.env.ORACLE_CLIENT_LIB_DIR; + if (libDir) { + oracledb.initOracleClient({ libDir }); + } else { + oracledb.initOracleClient(); + } + } +} catch (e) { + throw new Error(`Не удалось инициализировать Oracle Client (Thick-режим): ${makeErrorText(e)}`); +} + //-------------------------- // Глобальные идентификаторы //-------------------------- diff --git a/modules/sbis.js b/modules/sbis.js index ca6389b..d798860 100644 --- a/modules/sbis.js +++ b/modules/sbis.js @@ -8,7 +8,7 @@ //------------------------------ const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML -const httpRequest = require("../core/http_client"); //Работа с HTTP/HTTPS запросами +const { httpRequest } = require("../core/http_client"); //Работа с HTTP/HTTPS запросами const { SMCHD_STORAGE_SYSTEM } = require("./sbis_config"); //Система хранения МЧД //--------------------- diff --git a/package-lock.json b/package-lock.json index d9815df..c9f4b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "nodemailer": "^6.9.16", "oracledb": "^6.6.0", "pg": "^8.13.1", - "undici": "^6.0.0", "validate": "^5.1.0", "xml2js": "^0.6.2" }, @@ -40,9 +39,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.0.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.8.tgz", - "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -148,15 +147,15 @@ } }, "node_modules/broker-factory": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.12.tgz", - "integrity": "sha512-5Bmeki5j2IVO+lE07dSOUMZp1ZGKkE47b3ILv4ZD0nmTdc0iTKVS1CgYPDCy5m0Qb9jIKHBaF9SUrtqg5oW+1A==", + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz", + "integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "fast-unique-numbers": "^9.0.25", + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", "tslib": "^2.8.1", - "worker-factory": "^7.0.47" + "worker-factory": "^7.0.48" } }, "node_modules/buffer": { @@ -483,12 +482,12 @@ } }, "node_modules/fast-unique-numbers": { - "version": "9.0.25", - "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.25.tgz", - "integrity": "sha512-vHLSJfu0jSazb5X1jgYZIbsUd4mztxHxyFxUAPYvaYLkTsvQDn5+NbJRtfp+/tLIsUlMkD/geL2710QBxylH6w==", + "version": "9.0.26", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz", + "integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.28.6", "tslib": "^2.8.1" }, "engines": { @@ -946,12 +945,12 @@ } }, "node_modules/pg": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", - "integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==", + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.10.0", + "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", @@ -980,9 +979,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.0.tgz", - "integrity": "sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", "license": "MIT" }, "node_modules/pg-int8": { @@ -1428,15 +1427,6 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, - "node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -1482,50 +1472,50 @@ } }, "node_modules/worker-factory": { - "version": "7.0.47", - "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.47.tgz", - "integrity": "sha512-Ga5U8n7hJqovn98nlFnbyuJj66s8dCU4QOQd0dU0bje7uvrGGhOFeKtsTdB3b6fO5BD93F88rHpkBCGzgGloKw==", + "version": "7.0.48", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz", + "integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "fast-unique-numbers": "^9.0.25", + "@babel/runtime": "^7.28.6", + "fast-unique-numbers": "^9.0.26", "tslib": "^2.8.1" } }, "node_modules/worker-timers": { - "version": "8.0.28", - "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.28.tgz", - "integrity": "sha512-+AuNePH2P/PuhQURf5I+SIGBty4dq2CzoQEB+bMXIQiPrYj3WhkUtIW2bSzeETFWyXJFUdQGsyFeZtit15LkOw==", + "version": "8.0.29", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.29.tgz", + "integrity": "sha512-9jk0MWHhWAZ2xlJPXr45oe5UF/opdpfZrY0HtyPizWuJ+ce1M3IYk/4IIdGct3kn9Ncfs+tkZt3w1tU6KW2Fsg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.28.6", "tslib": "^2.8.1", - "worker-timers-broker": "^8.0.14", - "worker-timers-worker": "^9.0.12" + "worker-timers-broker": "^8.0.15", + "worker-timers-worker": "^9.0.13" } }, "node_modules/worker-timers-broker": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.14.tgz", - "integrity": "sha512-ooCGGWGcAYbWEJY2nkA60K9mZ33atvg/QIOBJ3OzdQJU5Z7/NdPFlEiMLiCYW8dpeP/qLcsaUsZzETrKNgGicg==", + "version": "8.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz", + "integrity": "sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", - "broker-factory": "^3.1.12", - "fast-unique-numbers": "^9.0.25", + "@babel/runtime": "^7.28.6", + "broker-factory": "^3.1.13", + "fast-unique-numbers": "^9.0.26", "tslib": "^2.8.1", - "worker-timers-worker": "^9.0.12" + "worker-timers-worker": "^9.0.13" } }, "node_modules/worker-timers-worker": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.12.tgz", - "integrity": "sha512-NBXCnKB/9CkhjWZz2dITgK94QM5GIJx+7LAlCA8mKeO6whdwmfH9S3iPEwakhn3+NOB9nHE3jQqdpKpZZJI23g==", + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz", + "integrity": "sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.28.4", + "@babel/runtime": "^7.28.6", "tslib": "^2.8.1", - "worker-factory": "^7.0.47" + "worker-factory": "^7.0.48" } }, "node_modules/wrappy": { diff --git a/package.json b/package.json index 6c72899..8ec1793 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "nodemailer": "^6.9.16", "oracledb": "^6.6.0", "pg": "^8.13.1", - "undici": "^6.0.0", "validate": "^5.1.0", "xml2js": "^0.6.2" },