ci-validation
Version:
🇺🇾 Complete TypeScript/JavaScript library for validating Uruguayan CI (Cédula de Identidad) with official algorithm and government service integration
1,190 lines • 60.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.NewCiService = void 0;
const axios_1 = __importDefault(require("axios"));
const cheerio_1 = require("cheerio");
const dotenv_1 = __importDefault(require("dotenv"));
const fs_1 = require("fs");
const https_1 = __importDefault(require("https"));
const path_1 = __importDefault(require("path"));
const tesseract_js_1 = require("tesseract.js");
const storage_1 = require("../storage");
const dateUtils_1 = require("../utils/dateUtils");
const personaUtils_1 = require("../utils/personaUtils");
dotenv_1.default.config();
class NewCiService {
constructor() {
this.targetUrl = "https://www.tramitesenlinea.mef.gub.uy/Apia/portal/tramite.jsp?id=2629";
this.serviceUrl = "https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.FormAction.run";
this.cookies = "";
this.sessionId = "unique-session-1";
this.email = "dev@cybersecurity.com";
this.httpsAgent = new https_1.default.Agent({
rejectUnauthorized: false, // Only ignore certs in development
keepAlive: true,
timeout: 10000,
maxSockets: 10,
maxFreeSockets: 10,
});
this.timestamp = Date.now();
this.introHeaders = {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "es-ES,es;q=0.9",
connection: "keep-alive",
host: "www.tramitesenlinea.mef.gub.uy",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
};
this.attId = "8461";
this.frmId = "6767";
}
/**
* Initialize session storage based on environment
*/
static async initializeSessionStorage() {
if (NewCiService.sessionStorage)
return;
try {
NewCiService.sessionStorage = await storage_1.SessionStorageFactory.createStorage({
expirationTime: 24 * 60 * 60 * 1000 * 10000, // Too much time.
autoCleanup: false,
});
console.log("✅ Session storage initialized");
}
catch (error) {
console.error("❌ Failed to initialize session storage:", error);
}
NewCiService.ensureOutputDirectory();
}
/**
* Ensures the output directory exists
*/
static async ensureOutputDirectory() {
try {
await fs_1.promises.mkdir(NewCiService.outputDir, { recursive: true });
}
catch (error) {
console.error("Error creating output directory:", error);
}
}
/**
* Save session using the new storage system
*/
async saveSession(tabId, tokenId, cookies, taskData) {
if (!NewCiService.sessionStorage)
return;
const sessionId = this.sessionId;
try {
await NewCiService.sessionStorage.saveSession(sessionId, {
...taskData,
tabId,
tokenId,
cookies,
document: sessionId.split("-")[2], // Extract document from session ID
createdAt: Date.now(),
lastUsed: Date.now(),
metadata: {
userAgent: this.introHeaders["user-agent"],
email: this.email,
},
});
console.log(`✅ Session saved with ID: ${sessionId}`);
}
catch (error) {
console.error("❌ Error saving session:", error);
}
}
/**
* Load session using the new storage system
*/
async loadSession() {
if (!NewCiService.sessionStorage) {
return null;
}
const sessionId = this.sessionId;
try {
const sessionData = await NewCiService.sessionStorage.loadSession(sessionId);
if (sessionData) {
console.log(`✅ Session loaded with ID: ${sessionId}`);
return {
proInstId: sessionData.proInstId,
proEleInstId: sessionData.proEleInstId,
tabId: sessionData.tabId,
tokenId: sessionData.tokenId,
cookies: sessionData.cookies,
};
}
return null;
}
catch (error) {
console.error("❌ Error loading session:", error);
return null;
}
}
/**
* Delete session using the new storage system
*/
async deleteSession() {
if (!NewCiService.sessionStorage)
return;
const sessionId = this.sessionId;
try {
await NewCiService.sessionStorage.deleteSession(sessionId);
console.log(`🗑️ Session deleted with ID: ${sessionId}`);
}
catch (error) {
console.error("❌ Error deleting session:", error);
}
}
jsonToQueryString(jsonData) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(jsonData)) {
params.append(key, value);
}
return `${params.toString()}`;
}
async sendCaptchaSol(cookies, httpsAgent, tokenId, tabId) {
const url = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=confirm&asXML=true&appletToken=&tabId=${tabId}&tokenId=${tokenId}`;
const captchaSol = "354pn";
const body = `1754148664040E_1361=${captchaSol}`;
}
getFields(xml) {
const $ = (0, cheerio_1.load)(xml, { xmlMode: true });
// Buscamos los valores específicos por attName
const getFieldValue = (attName) => $(`field[attName='${attName}']`).attr("value") || null;
const cedula = getFieldValue("CRMRCSPS_NUMERO_DE_DOCUMENTO_STR");
const primerNombre = getFieldValue("TRM_PERSONA_FISICA_NOMBRE_PRIMER_STR");
const segundoNombre = getFieldValue("TRM_PERSONA_FISICA_NOMBRE_SEGUNDO_STR");
const primerApellido = getFieldValue("TRM_PERSONA_FISICA_APELLIDO_PRIMER_STR");
const segundoApellido = getFieldValue("TRM_PERSONA_FISICA_APELLIDO_SEGUNDO_STR");
const fechaNacimiento = getFieldValue("CRMRCSPS_FECHA_DE_NACIMIENTO_DATE");
const nombres = [primerNombre, segundoNombre].filter(Boolean).join(" ");
const apellidos = [primerApellido, segundoApellido].filter(Boolean).join(" ");
return {
cedula,
nombres,
apellidos,
fechaNacimiento,
};
}
async fireFinalEvent(tokenId, tabId, cookie) {
const frmId = "6648";
const attId = "8461";
const html = await this.fireEventSingle(tokenId, tabId, frmId, attId, cookie);
const $ = (0, cheerio_1.load)(html);
const htmlToParse = $("#E_6648").attr("data-xml");
if (!htmlToParse) {
throw new Error("#E_6648_not_found");
}
const fields = this.getFields(htmlToParse);
return { ...fields, hasSession: false, hasRefreshed: false };
}
async fireEvents(tokenId, tabId) {
// Aceptar términos. 1 -> Aceptar términos
const events = [
// Formulario SII!!!
{
frmId: "6687",
attId: "12295",
value: "1",
},
{
frmId: "6368",
attId: "10988",
value: "",
},
{
frmId: "6368",
attId: "10989",
value: "1675089513605.png",
},
{
frmId: "6368",
attId: "10990",
value: "",
},
{
frmId: "6368",
attId: "10991",
value: "Usuario anónimo",
},
{
frmId: "6368",
attId: "10992",
value: "1742495732743.png",
},
{
frmId: "6368",
attId: "10993",
value: "https://www.gub.uy/ministerio-economia-finanzas/",
},
{
frmId: "6368",
attId: "1227",
value: "1",
},
{
frmId: "6368",
attId: "1228",
value: "1",
},
{
frmId: "6368",
attId: "1236",
value: "1",
},
{
frmId: "6368",
attId: "1239",
value: "Consulta/Reclamación o Denuncia en Materia de Relaciones de Consumo",
},
{
frmId: "6368",
attId: "1265",
value: "54",
},
{
frmId: "6368",
attId: "1269",
value: this.email,
},
{
frmId: "6368",
attId: "1270",
value: "1",
},
{
frmId: "6368",
attId: "12792",
value: "",
},
{
frmId: "6368",
attId: "12800",
value: "",
},
{
frmId: "6368",
attId: "12801",
value: "",
},
{
frmId: "6368",
attId: "12802",
value: "",
},
{
frmId: "6368",
attId: "12803",
value: "",
},
{
frmId: "6368",
attId: "12804",
value: "",
},
{
frmId: "6368",
attId: "12805",
value: "",
},
{
frmId: "6368",
attId: "12806",
value: "",
},
{
frmId: "6368",
attId: "1569",
value: "1",
},
{
frmId: "6368",
attId: "1570",
value: "2",
},
{
frmId: "6368",
attId: "1594",
value: "2629",
},
{
frmId: "6368",
attId: "1604",
value: "",
},
{
frmId: "6368",
attId: "1924",
value: "SI",
},
{
frmId: "6368",
attId: "1925",
value: "",
},
{
frmId: "6368",
attId: "1929",
value: "",
},
{
frmId: "6368",
attId: "3957",
value: "",
},
{
frmId: "6368",
attId: "4304",
value: "",
},
{
frmId: "6368",
attId: "4305",
value: "",
},
{
frmId: "6368",
attId: "5513",
value: "",
},
{
frmId: "6368",
attId: "5514",
value: "",
},
{
frmId: "6368",
attId: "5522",
value: "1",
},
{
frmId: "6368",
attId: "7378",
value: "",
},
{
frmId: "6368",
attId: "7618",
value: "2",
},
{
frmId: "6368",
attId: "7620",
value: "1",
},
{
frmId: "6368",
attId: "7622",
value: "",
},
{
frmId: "6368",
attId: "7845",
value: "WEB_PC",
},
{
frmId: "6368",
attId: "7862",
value: "externo",
},
{
frmId: "6368",
attId: "7922",
value: "",
},
];
let prEvents = [];
for (const { frmId, attId, value } of events) {
if (frmId)
prEvents.push(this.submitEntry(tokenId, tabId, attId, frmId, value));
}
await Promise.all(prEvents);
}
async fireAgreement(tokenId, tabId) {
await this.fireEvents(tokenId, tabId);
await this.checkSignableForms(tabId, tokenId);
}
async firePersonaFisicaEvent(tokenId, tabId, cookie) {
await this.fireAgreement(tokenId, tabId);
const json = {
frmId: "6647",
attId: "8459",
value: "1",
};
await this.submitEntry(tokenId, tabId, json.attId, json.frmId, "1");
await this.fireEventSingle(tokenId, tabId, json.frmId, json.attId);
}
async fireEventSingle(tokenId, tabId, frmId, attId, cookie) {
const url = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.FormAction.run?action=fireFieldEvent¤tTab=forms~0&tabId=${tabId}&tokenId=${tokenId}&fldId=2&frmId=${frmId}&frmParent=E&index=0&evtId=1&attId=${attId}&react=true`;
const headers = this.getDefaultHeaders();
if (cookie) {
headers.Cookie = cookie;
}
const res = await axios_1.default
.post(url, "", {
headers,
httpsAgent: this.httpsAgent,
})
.catch((e) => {
console.log("Status", e.response.status, url);
return { data: "" };
});
return res.data;
}
/**
* Extrae proInstId y proEleInstId de una respuesta XML
* @param xmlResponse - La respuesta XML que contiene la URL con los parámetros
* @returns Objeto con proInstId y proEleInstId extraídos
*/
extractProcessInstanceIds(xmlResponse) {
try {
console.log(`🔍 Extracting process instance IDs from XML response...`);
// Buscar el atributo url en el XML
const urlMatch = xmlResponse.match(/url="([^"]+)"/);
if (!urlMatch) {
console.error("❌ No URL attribute found in XML response");
return null;
}
let url = urlMatch[1];
// Decodificar entidades HTML (& -> &)
url = url.replace(/&/g, "&");
console.log(`📡 Extracted URL: ${url}`);
// Extraer proInstId y proEleInstId usando regex
const proInstIdMatch = url.match(/proInstId=([^&]+)/);
const proEleInstIdMatch = url.match(/proEleInstId=([^&]+)/);
if (!proInstIdMatch || !proEleInstIdMatch) {
console.error("❌ proInstId or proEleInstId not found in URL");
return null;
}
const result = {
proInstId: proInstIdMatch[1],
proEleInstId: proEleInstIdMatch[1],
};
console.log(`✅ Extracted process IDs:`, result);
return result;
}
catch (error) {
console.error("❌ Error extracting process instance IDs:", error);
return null;
}
}
/**
* Convierte parámetros de URL a objeto JSON
* @param urlParams - Cadena de parámetros URL (ej: "&tabId=123&tokenId=456")
* @returns Objeto JSON con los parámetros parseados
*/
parseUrlParamsToJson(urlParams) {
try {
// Remover el & inicial si existe
const cleanParams = urlParams.startsWith("&") ? urlParams.substring(1) : urlParams;
// Usar URLSearchParams para parsear de forma segura
const searchParams = new URLSearchParams(cleanParams);
const result = {};
// Convertir a objeto
searchParams.forEach((value, key) => {
result[key] = value;
});
console.log(`🔍 Parsed URL params:`, result);
return result;
}
catch (error) {
console.error("❌ Error parsing URL params:", error);
return {};
}
}
/**
* Extrae específicamente tabId y tokenId de parámetros URL
* @param urlParams - Cadena de parámetros URL
* @returns Objeto con tabId y tokenId
*/
extractTabAndTokenIds(urlParams) {
try {
const params = this.parseUrlParamsToJson(urlParams);
return {
tabId: params.tabId,
tokenId: params.tokenId,
};
}
catch (error) {
console.error("❌ Error extracting tab and token IDs:", error);
return { tabId: "", tokenId: "" };
}
}
/**
* Extrae el valor de TAB_ID_REQUEST del HTML response
* @param htmlContent - El contenido HTML donde buscar
* @returns El valor de TAB_ID_REQUEST o null si no se encuentra
*/
extractTabIdRequest(htmlContent) {
try {
// Expresión regular para capturar el valor entre var TAB_ID_REQUEST y ;
const tabIdPattern = /var\s+TAB_ID_REQUEST\s*=\s*['"]*([^'";]+)['"]*\s*;/i;
const match = htmlContent.match(tabIdPattern);
if (match && match[1]) {
const tabIdValue = match[1].trim();
console.log(`🔍 Extracted TAB_ID_REQUEST: ${tabIdValue}`);
return this.extractTabAndTokenIds(tabIdValue);
}
console.log("⚠️ TAB_ID_REQUEST not found in HTML content");
return null;
}
catch (error) {
console.error("❌ Error extracting TAB_ID_REQUEST:", error);
return null;
}
}
getDefaultHeaders() {
return {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "es-ES,es;q=0.9",
"Cache-Control": "max-age=0",
Connection: "keep-alive",
"Content-Type": "application/x-www-form-urlencoded",
Cookie: this.cookies,
Origin: "https://www.tramitesenlinea.mef.gub.uy",
Referer: "https://www.tramitesenlinea.mef.gub.uy/Apia/portal/tramite.jsp?id=2629",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
};
}
async redirect() {
const data = "onFinishURL=https%3A%2F%2Fwww.tramitesenlinea.mef.gub.uy%2FApia%2Fportal%2FredirectSLO.jsp&id=2629&tabId=1&lang=1&eatt_STR_TRM_COD_TRAMITE_STR=54&react=true&eatt_STR_TRM_MODO_AUTENTICACION_STR=1&logFromFile=true&proCode=1163&eatt_STR_TRM_CANAL_AUTENTICACION_STR=IDURUGUAY&env=1&onFinish=5&type=P&eatt_STR_TRM_VISIBILIDAD_STR=1&entCode=1006&eatt_str_TRM_CANAL_INICIO_STR=WEB_PC";
const url = "https://www.tramitesenlinea.mef.gub.uy/Apia/page/externalAccess/open.jsp";
// Perform the GET request
const response = await axios_1.default.post(url, data, {
timeout: 10000, // 10 second timeout
httpsAgent: this.httpsAgent, // Use the custom HTTPS agent
maxRedirects: 5, // Follow up to 5 redirects
headers: this.getDefaultHeaders(),
});
console.log("Status", response.status);
const cookies = this.extractCookiesStr(response);
//this.cookies = this.extractCookiesStr(response);
const tabReq = this.extractTabIdRequest(response.data);
if (!tabReq)
throw new Error("Could not extract tabId and tokenId from response");
const { tabId, tokenId } = tabReq;
//
return { tabId, tokenId };
}
async submitEntry(tokenId, tabId, attId, frmId, value) {
const timestamp = Date.now();
const url = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.FormAction.run?action=processFieldSubmit&isAjax=true&react=true&tabId=${tabId}&tokenId=${tokenId}×tamp=${timestamp}&attId=${attId}&frmId=${frmId}&index=0&frmParent=E×tamp=${timestamp}`;
const body = `value=${encodeURIComponent(value)}`;
const headers = this.getDefaultHeaders();
// Perform the GET request
const res = await axios_1.default.post(url, body, {
timeout: 10000, // 10 second timeout
httpsAgent: this.httpsAgent, // Use the custom HTTPS agent
maxRedirects: 5, // Follow up to 5 redirects
validateStatus: (status) => status < 500, // Accept all status codes below 500
headers,
});
if (!res.data.includes('success="true"') || res.data.includes("Error del sistema")) {
console.error("Roto", frmId, attId, value);
if (attId === "12295")
throw new Error("Agreement broken, stop");
}
return res.data && res.data.includes('success="true"');
}
async fillEmail(tokenId, tabId) {
const attId = "11638";
const frmId = "1361";
await this.submitEntry(tokenId, tabId, attId, frmId, this.email);
}
async queryCy(document, tokenId, tabId) {
const attId = "8461";
const frmId = "6648";
const res = await this.submitEntry(tokenId, tabId, attId, frmId, document);
return res;
}
getCookieString(cookies) {
const cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join(";");
return cookieString;
}
async generateTask(tabId, tokenId, formData) {
const url = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=checkWizzard&tabId=${tabId}&tokenId=${tokenId}`;
const res = await axios_1.default.post(url, "", {
timeout: 5000,
httpsAgent: this.httpsAgent,
headers: this.getDefaultHeaders(),
});
const data = res.data;
const proInstIds = this.extractProcessInstanceIds(data);
if (!proInstIds)
throw new Error("Unable to track process");
const { proInstId, proEleInstId } = proInstIds;
const urlAgain = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=getTask&proInstId=${proInstId}&proEleInstId=${proEleInstId}&fromWizzard=true&tabId=${tabId}&tokenId=${tokenId}&react=true`;
// Send captcha again
await axios_1.default.post(urlAgain, formData, {
timeout: 5000,
httpsAgent: this.httpsAgent,
headers: this.getDefaultHeaders(),
});
return { proInstId, proEleInstId };
}
/**
* Extrae la URL del workArea desde el código JavaScript embebido en HTML
* @param htmlContent - El contenido HTML que contiene el script con workArea
* @returns La URL extraída o null si no se encuentra
*/
extractWorkAreaSrc(htmlContent) {
try {
if (!htmlContent || typeof htmlContent !== "string") {
console.warn("⚠️ Invalid HTML content provided to extractWorkAreaSrc");
return null;
}
// Patrón para buscar document.getElementById("workArea").src="[URL]"
const workAreaPattern = /document\.getElementById\s*\(\s*["']workArea["']\s*\)\s*\.src\s*=\s*["']([^"']+)["']/i;
const match = htmlContent.match(workAreaPattern);
if (match && match[1]) {
const extractedUrl = match[1].trim();
console.log(`✅ WorkArea URL extraída: ${extractedUrl}`);
return extractedUrl;
}
console.log("⚠️ No se encontró workArea.src en el contenido HTML");
return null;
}
catch (error) {
console.error("❌ Error extrayendo workArea.src:", error);
return null;
}
}
async refreshCookies(proInstId, proEleInstId, tabId, tokenId) {
try {
const nroTramite = parseInt(proInstId) - 8157;
const url = `https://www.tramitesenlinea.mef.gub.uy/Apia/page/externalAccess/workTask.jsp?logFromFile=true&env=1&lang=1&numInst=TRM_PRTL_${nroTramite}&onFinish=5&onFinishURL=https://www.gub.uy/ministerio-economia-finanzas/&eatt_str_TRM_RETOMA_TRAMITE_STR=SI&eat_str_TRM_ACCESO_EXTERNO_STR=true`;
const headers = this.getDefaultHeaders();
headers.Cookie = "";
const response = await axios_1.default.get(url, {
timeout: 10000,
httpsAgent: this.httpsAgent,
headers,
});
const cookies = this.extractCookiesStr(response);
if (response.data.includes(proInstId)) {
console.log("Task matches");
headers.Cookie = cookies;
const newUrl = this.extractWorkAreaSrc(response.data);
if (!newUrl) {
console.error("❌ No se pudo extraer la URL del workArea");
throw new Error("work_area_url_not_found");
}
const toGo = `https://www.tramitesenlinea.mef.gub.uy/${newUrl}`;
const newTabId = newUrl.split("tabId=")[1].split("&")[0];
const newTokenId = newUrl.split("tokenId=")[1].split("&")[0];
if (!newTabId || !newTokenId) {
console.error("❌ No se pudo extraer tabId o tokenId de la URL del workArea");
throw new Error("tab_or_token_id_not_found");
}
const resF = await axios_1.default.get(toGo, {
timeout: 10000,
httpsAgent: this.httpsAgent,
headers: headers,
});
return {
cookies: cookies,
tabId: newTabId,
tokenId: newTokenId,
};
}
throw new Error("redirect_task_not_matching");
}
catch (e) {
console.error(e);
return {
cookies: "",
tabId: "",
tokenId: "",
};
}
}
/**
* Performs a GET request to the MEF portal and saves the response
* @param document - The document number to check
* @param options - Optional parameters for the check
* @param options.ignoreCache - If true, bypasses cache and performs a fresh check
* @returns Promise<any> - The response data
*/
async check(document, options, att = 0) {
let hasRefreshed = false;
try {
// ignoreCache not implemented.
console.log(`Checking document number: ${document}`);
// Try to load existing session for this document
const existingSession = await this.loadSession();
let tokenId = existingSession?.tokenId;
let tabId = existingSession?.tabId;
this.cookies = existingSession?.cookies;
let proInstId = existingSession?.proInstId;
let proEleInstId = existingSession?.proEleInstId;
const hasSession = !!proInstId;
if (hasSession) {
const sessionWorking = await this.queryCy(document, tokenId, tabId);
if (!sessionWorking || options?.forceRefresh) {
console.log(`🔄 Reusing existing session: tabId=${tabId}, tokenId=${tokenId}`);
const resCookies = await this.refreshCookies(proInstId, proEleInstId, tabId, tokenId);
console.log("ResCookies", resCookies);
if (resCookies.cookies) {
hasRefreshed = true;
this.cookies = resCookies.cookies;
tabId = resCookies.tabId;
tokenId = resCookies.tokenId;
console.log(`🔄 Cookies refreshed: tabId=${tabId}, tokenId=${tokenId}`);
}
}
}
if (!hasSession) {
const shouldIgnoreCache = options?.ignoreCache || false;
if (shouldIgnoreCache) {
console.log(`🚫 Cache bypass requested, performing fresh check for: ${document}`);
}
// Perform the GET request
const response = await axios_1.default.get(this.targetUrl, {
timeout: 10000, // 10 second timeout
httpsAgent: this.httpsAgent, // Use the custom HTTPS agent
maxRedirects: 10, // Follow up to 5 redirects
headers: this.introHeaders,
});
this.cookies = this.extractCookiesStr(response);
const res1 = await this.redirect();
tabId = res1.tabId;
tokenId = res1.tokenId;
console.log(`🔍 Redirected to tabId: ${tabId}, tokenId: ${tokenId}`);
await this.fillEmail(tokenId, tabId);
let captchaSolved = "";
let formData = "";
let nextStepResult = null;
let att = 0;
while (!captchaSolved && att < 10) {
console.log("Att", att, 10);
nextStepResult = await this.performNextStepRequest(tabId, tokenId);
captchaSolved = nextStepResult.captchaSolved;
formData = nextStepResult.formData;
att++;
}
if (!formData) {
throw new Error("captcha_not_solved");
}
const prIds = await this.generateTask(tabId, tokenId, formData);
proInstId = prIds.proInstId;
proEleInstId = prIds.proEleInstId;
await this.firePersonaFisicaEvent(tokenId, tabId);
}
await this.queryCy(document, tokenId, tabId);
let res = await this.fireFinalEvent(tokenId, tabId);
res.hasSession = hasSession;
res.hasRefreshed = hasRefreshed;
if (res && res.cedula) {
console.log(`✅ Document ${document} found:`, res);
// Save session using new storage system
await this.saveSession(tabId, tokenId, this.cookies, { proInstId, proEleInstId });
}
else {
// No existe persona con esa identificación
res = {
cedula: "",
nombres: "",
apellidos: "",
fechaNacimiento: "",
hasSession,
hasRefreshed,
};
}
return res;
}
catch (error) {
// Clear session on error
await this.deleteSession();
console.error(`❌ Error checking document ${document}:`, error);
// Save error response to JSON file
if (error?.message === "#E_6648_not_found" && att < 3) {
console.log("Retrying due to missing #E_6648 element...");
return this.check(document, { ignoreCache: false, forceRefresh: false }, att + 1);
}
throw error;
}
}
async isServiceAvailable() {
// Not done.
return true;
}
async queryCiInfo(ci) {
await NewCiService.initializeSessionStorage();
try {
const res = await this.check(ci);
// Procesar fecha de nacimiento si existe
let fechaNacimientoDate = null;
let edad = null;
if (res.fechaNacimiento) {
try {
const fechaInfo = dateUtils_1.DateUtils.procesarFechaNacimiento(res.fechaNacimiento);
if (fechaInfo) {
fechaNacimientoDate = fechaInfo.fechaDate;
edad = fechaInfo.edad;
}
}
catch (error) {
console.warn("Error procesando fecha de nacimiento:", error);
}
}
// Analizar información adicional de la persona
const infoAdicional = personaUtils_1.PersonaUtils.analizarPersona(res.nombres, res.apellidos, edad || undefined);
return {
success: true,
data: {
persona: {
nombre: res.nombres,
apellido: res.apellidos,
fechaNacimiento: res.fechaNacimiento,
fechaNacimientoDate: fechaNacimientoDate,
edad: edad,
cedula: res.cedula,
// Información adicional
genero: infoAdicional.genero,
iniciales: infoAdicional.iniciales,
nombreCompleto: infoAdicional.nombreCompleto,
longitudNombre: infoAdicional.longitudNombre,
tieneSegundoNombre: infoAdicional.tieneSegundoNombre,
cantidadNombres: infoAdicional.cantidadNombres,
generacion: infoAdicional.generacion,
},
message: res.error || "Consulta exitosa",
status: 200,
},
};
}
catch (error) {
return this.handleError(error);
}
}
/**
* Maneja errores de la consulta externa
*/
handleError(error) {
if (axios_1.default.isAxiosError(error)) {
if (error.code === "ECONNABORTED") {
return {
success: false,
error: "Timeout: El servicio no respondió en el tiempo esperado",
};
}
if (error.response) {
return {
success: false,
error: `Error del servidor: ${error.response.status} - ${error.response.statusText}`,
};
}
if (error.request) {
return {
success: false,
error: "Error de conexión: No se pudo conectar con el servicio",
};
}
}
return {
success: false,
error: `Error inesperado: ${error.message || "Error desconocido"}`,
};
}
async checkWithCookies(document, cookie, tokenId, tabId) {
this.cookies = cookie;
await this.firePersonaFisicaEvent(tokenId, tabId, cookie);
await this.queryCy(document, tokenId, tabId);
const res = await this.fireFinalEvent(tokenId, tabId);
return res;
}
extractCookiesStr(response) {
const cookies = this.extractCookies(response);
return this.getCookieString(cookies);
}
/**
* Extracts cookies from the response headers
* @param response - The axios response object
* @returns Array of cookie information
*/
extractCookies(response) {
const cookies = [];
const setCookieHeader = response.headers["set-cookie"];
if (setCookieHeader) {
setCookieHeader.forEach((cookieString) => {
const cookieParts = cookieString.split(";");
const [nameValue] = cookieParts;
const [name, value] = nameValue.split("=");
if (name && value && name === "JSESSIONID") {
cookies.push({
name: name.trim(),
value: value.trim(),
domain: this.extractCookieAttribute(cookieParts, "domain"),
path: this.extractCookieAttribute(cookieParts, "path"),
});
}
});
}
return cookies;
}
/**
* Extracts a specific attribute from cookie parts
* @param cookieParts - Array of cookie parts
* @param attribute - The attribute to extract
* @returns The attribute value or undefined
*/
extractCookieAttribute(cookieParts, attribute) {
for (const part of cookieParts) {
if (part
.trim()
.toLowerCase()
.startsWith(attribute.toLowerCase() + "=")) {
return part.split("=")[1]?.trim();
}
}
return undefined;
}
async fetchExtractedUrl(url, httpsAgent, cookieString) {
const iframeResponse = await axios_1.default
.get(url, {
timeout: 10000,
httpsAgent: httpsAgent,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "es-ES,es;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
Connection: "keep-alive",
Cookie: cookieString,
Referer: this.targetUrl,
"Sec-Fetch-Dest": "iframe",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "same-origin",
"Upgrade-Insecure-Requests": "1",
},
})
.then((res) => res.data);
return iframeResponse;
}
async asyncCheckAction(url, tabId, tokenId) {
const response = await axios_1.default.post(url + `&tabId=${tabId}&tokenId=${tokenId}`, "", {
timeout: 10000,
httpsAgent: this.httpsAgent,
headers: this.getDefaultHeaders(),
});
return {
url,
tabId,
tokenId,
timestamp: new Date().toISOString(),
status: response.status,
statusText: response.statusText,
headers: response.headers,
responseData: response.data,
hasSignableForms: response.data && (response.data.includes("true") || response.data.includes("signable")),
};
}
/**
* Checks if there are signable forms before proceeding to next step
* @param refererUrl - The referer URL from the extracted iframe URL
* @param tabId - The tab ID from previous requests
* @param tokenId - The token ID from previous requests
* @param cookies - Session cookies
* @param httpsAgent - HTTPS agent for requests
* @returns Promise<any> - The signable forms check response
*/
async checkSignableForms(tabId, tokenId) {
try {
const documentsLockUrl = "https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=checkWebDavDocumentsLocks&isAjax=true";
// Build the hasSignableForms URL
const signableFormsUrl = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=hasSignableForms&appletToken=`;
const goToNextUrl = `https://www.tramitesenlinea.mef.gub.uy/Apia/apia.execution.TaskAction.run?action=gotoNextStep¤tTab=forms~32&fromPanel=null&react=true`;
//await this.asyncCheckAction(documentsLockUrl, tabId, tokenId);
//const res = await this.asyncCheckAction(signableFormsUrl, tabId, tokenId);
await this.asyncCheckAction(goToNextUrl, tabId, tokenId);
//return res;s
// Create cookie string for requests
}
catch (error) {
console.error(`❌ Error checking signable forms:`, error);
return {
tabId,
tokenId,
responseData: error.response?.data,
error: true,
errorMessage: error instanceof Error ? error.message : "Unknown error",
timestamp: new Date().toISOString(),
};
}
}
/**
* Solves CAPTCHA using OCR with serverless-compatible configuration
* @param captchaUrl - The URL of the CAPTCHA image
* @param cookies - Session cookies
* @param httpsAgent - HTTPS agent for requests
* @returns Promise<string> - The solved CAPTCHA text
*/
async solveCaptcha(captchaUrl) {
try {
// Download the CAPTCHA image
const imageResponse = await axios_1.default.get(captchaUrl, {
timeout: 10000,
httpsAgent: this.httpsAgent,
responseType: "arraybuffer",
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "es-ES,es;q=0.9",
Cookie: this.cookies,
Host: "www.tramitesenlinea.mef.gub.uy",
Referer: "https://www.tramitesenlinea.mef.gub.uy/Apia/page/externalAccess/open.jsp",
"Sec-Fetch-Dest": "image",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-origin",
},
});
// For Vercel serverless environment, use external OCR API
const hasApiOcr = !!process.env.OCR_API_URL;
if (hasApiOcr) {
console.log("🌐 Running in Vercel serverless environment - using external OCR API");
const captchaBase64 = `data:image/png;base64,${imageResponse.data.toString("base64")}`;
try {
// Use external OCR API to avoid WASM issues
const ocrApiUrl = process.env.OCR_API_URL || "http://localhost:3001";
const ocrResponse = await axios_1.default.post(`${ocrApiUrl}/ocr/captcha`, {
imageUrl: captchaBase64,
cookies: this.cookies,
useAdvanced: true,
options: {
whitelist: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
serverless: true,
},
}, {
timeout: 30000, // 30 second timeout for OCR processing
headers: {
"Content-Type": "application/json",
"User-Agent": "PhoneChecker/1.0",
},
});
console.log("Ocr response", ocrResponse.data);
if (ocrResponse.data.success && ocrResponse.data.cleanedText) {
const cleanText = ocrResponse.data.cleanedText;
console.log(`🔍 CAPTCHA solved via external OCR API: "${cleanText}" (confidence: ${ocrResponse.data.confidence})`);
// Save captcha with the name as cleanText in /captcha_solved folder.
return cleanText;
}
else {
throw new Error(`External OCR API failed: ${ocrResponse.data.error || "Unknown error"}`);
}
}
catch (externalOcrError) {
console.error("❌ External OCR API failed:", externalOcrError);
console.log("🔄 Using smart CAPTCHA fallback for serverless environment");
// Return a smart fallback value
return this.generateSmartCaptchaFallback();
}
}
// Save the CAPTCHA image temporarily (use /tmp in serverless environments)
const tempDir = process.env.VERCEL ? "/tmp" : NewCiService.outputDir;
const captchaImagePath = path_1.default.join(tempDir, `captcha_${Date.now()}.png`);
await fs_1.promises.writeFile(captchaImagePath, imageResponse.data);
console.log(`📄 CAPTCHA image saved to: ${captchaImagePath}`);
// Local development - use full featured OCR
console.log("🏠 Running in local environment - using advanced OCR");
// Initialize Tesseract worker with better language support
const worker = await (0, tesseract_js_1.createWorker)(["eng"]);
// Configure Tesseract for optimal CAPTCHA recognition
await worker.setParameters({
// Character whitelist - only alphanumeric characters
tessedit_char_whitelist: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
// Page segmentation mode - treat the image as a single text line
tessedit_pageseg_mode: tesseract_js_1.PSM.SINGLE_LINE,
// OCR Engine Mode - use both legacy and LSTM engines for better accuracy
tessedit_ocr_engine_mode: tesseract_js_1.OEM.TESSERACT_LSTM_COMBINED,
// Improve recognition for small text
tessedit_do_invert: "1",
// Additional parameters for better CAPTCHA recognition
classify_enable_learning: "0",
classify_enable_adaptive_matcher: "0",
textord_really_old_xheight: "1",
textord_min_xheight: "10",
preserve_interword_spaces: "0",
// Improve edge detection
edges_max_children_per_outline: "40",
// Noise reduction
textord_noise_sizelimit: "0.5",
// Improve character recognition
tessedit_char_unblacklist: "",
// Better handling of small fonts
textord_min_linesize: "2.5",
});
// Recognize text from the CAPTCHA image with multiple attempts
let recognitionResults = [];
// Try recognition with different configurations
const configs = [
{ psr: tesseract_js_1.PSM.SINGLE_LINE, oem: tesseract_js_1.OEM.TESSERACT_LSTM_COMBINED },
{ psr: tesseract_js_1.PSM.SINGLE_WORD, oem: tesseract_js_1.OEM.LSTM_ONLY },
{ psr: tesseract_js_1.PSM.SINGLE_CHAR, oem: tesseract_js_1.OEM.TESSERACT_ONLY },
{ psr: tesseract_js_1.PSM.RAW_LINE, oem: tesseract_js_1.OEM.TESSERACT_LSTM_COMBINED },
];
for (const config of configs) {
try {
await worker.setParameters({
tessedit_pageseg_mode: config.psr,
tessedit_ocr_engine_mode: config.oem,
});
const { data: { text, confidence }, } = await worker.recognize(captchaImagePath);
const cleanText = this.cleanCaptchaText(text);
if (cleanText && cleanText.length >= 4 && cleanText.length <= 8) {
recognitionResults.push({
text: cleanText,
confidence: confidence,
config: config,
});
}
}
catch (configError) {
console.warn(`Recognition attempt failed with config:`, config, configError);
}
}
// Sort by confidence and length preference
recognitionResults.sort((a, b) => {
// Prefer results with length 5-6 (typical CAPTCHA length)
const aLengthScore = Math.abs(a.text.length - 5.5);
const bLengthScore = Math.abs(b.text.length - 5.5);
if (Math.abs(aLengthScore - bLengthScore) > 0.5) {
return aLengthScore - bLengthScore;
}
// Then prefer higher confidence
return b.confidence - a.confidence;
});
const bestResult = recognitionResults[0];
const cleanText = bestResult ? bestResult.text : this.cleanCaptchaText((await worker.recognize(captchaImagePath)).data.text);
console.log(`🔍 CAPTCHA recognition results:`, recognitionResults.slice(0, 3));
console.log(`🔍 CAPTCHA solved: "${cleanText}" (confidence: ${bestResult?.confidence || "unknown"})`);
// Clean up
await worker.terminate();
// Optionally delete the temporary image file
try {
await fs_1.promises.unlink(captchaImagePath);
console.log(`🗑️ Temporary CAPTCHA image deleted: ${captchaImagePath}`);
}
catch (error) {
console.warn(`Warning: Could not delete temporary CAPTCHA image: ${error}`);
}
// Validate the result
if (!cleanText || cleanTex