Compare commits

..

3 Commits

Author SHA1 Message Date
Horux Dev
b39bbcdd0a feat(sat): integra ProxyManager en createSatService
- Usa proxyManager.createNextAgent() para obtener un proxy del pool.
- Pasa el agente al HttpsWebClient parcheado.
- Log indica si se usa proxy o IP directa.
- Comportamiento actual sin cambios si SAT_PROXY_LIST está vacío.
2026-08-03 04:35:19 +00:00
Horux Dev
5489c84e6b feat(sat): parchea librería SAT para soportar agente proxy
- HttpsWebClient acepta un cuarto parámetro opcional 'agent'.
- El agente se pasa a https.request() para tunelar conexiones.
- Mantiene el parche anterior de fallback para getResponse.
2026-08-03 04:30:44 +00:00
Horux Dev
24d35333df feat(sat): agrega ProxyManager para rotación de proxies
- Nuevo servicio apps/api/src/services/sat/proxy.service.ts.
- Soporta estrategias round-robin y random.
- Parsea SAT_PROXY_LIST y crea agentes HttpsProxyAgent.
- Script scripts/test-proxy-rotation.ts para validar configuración.
2026-08-03 04:07:58 +00:00
5 changed files with 159 additions and 5 deletions

View File

@@ -0,0 +1,20 @@
import 'dotenv/config';
import { ProxyManager } from '../src/services/sat/proxy.service.js';
const manager = new ProxyManager(
process.env.SAT_PROXY_LIST || '',
(process.env.SAT_PROXY_STRATEGY as any) || 'round-robin',
);
console.log(`Total de proxies: ${manager.getProxyCount()}`);
console.log(`Estrategia: ${process.env.SAT_PROXY_STRATEGY || 'round-robin'}`);
console.log('Próximos 10 proxies seleccionados:');
for (let i = 0; i < 10; i++) {
const proxy = manager.getNextProxy();
if (!proxy) {
console.log(` ${i + 1}. (sin proxy configurado)`);
} else {
console.log(` ${i + 1}. ${proxy.host}:${proxy.port}`);
}
}

View File

@@ -0,0 +1,100 @@
import { HttpsProxyAgent } from 'https-proxy-agent';
export interface ProxyConfig {
url: string;
host: string;
port: number;
username?: string;
password?: string;
}
export type ProxyStrategy = 'round-robin' | 'random';
function parseProxyList(raw: string): ProxyConfig[] {
if (!raw.trim()) return [];
const configs: ProxyConfig[] = [];
const items = raw.split(',').map(s => s.trim()).filter(Boolean);
for (const item of items) {
try {
const url = new URL(item);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
console.warn(`[ProxyManager] Protocolo no soportado, se omite: ${item}`);
continue;
}
configs.push({
url: item,
host: url.hostname,
port: Number(url.port) || (url.protocol === 'https:' ? 443 : 80),
username: url.username || undefined,
password: url.password || undefined,
});
} catch (err) {
console.warn(`[ProxyManager] URL de proxy inválida, se omite: ${item}`);
}
}
return configs;
}
export class ProxyManager {
private proxies: ProxyConfig[];
private strategy: ProxyStrategy;
private currentIndex = 0;
constructor(
proxyList = process.env.SAT_PROXY_LIST || '',
strategy: ProxyStrategy = (process.env.SAT_PROXY_STRATEGY as ProxyStrategy) || 'round-robin',
) {
this.proxies = parseProxyList(proxyList);
this.strategy = ['round-robin', 'random'].includes(strategy) ? strategy : 'round-robin';
if (this.proxies.length > 0) {
console.log(`[ProxyManager] ${this.proxies.length} proxy(s) configurados (estrategia: ${this.strategy})`);
} else {
console.log('[ProxyManager] No hay proxies configurados; se usará la IP directa del servidor');
}
}
hasProxies(): boolean {
return this.proxies.length > 0;
}
getProxyCount(): number {
return this.proxies.length;
}
getNextProxy(): ProxyConfig | null {
if (this.proxies.length === 0) return null;
if (this.strategy === 'random') {
return this.proxies[Math.floor(Math.random() * this.proxies.length)];
}
const proxy = this.proxies[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
return proxy;
}
createAgent(proxy: ProxyConfig): HttpsProxyAgent<string> {
return new HttpsProxyAgent(proxy.url);
}
/**
* Crea un agente con el siguiente proxy del pool.
* Útil cuando se quiere una nueva conexión por solicitud.
*/
createNextAgent(): HttpsProxyAgent<string> | null {
const proxy = this.getNextProxy();
if (!proxy) return null;
console.log(`[ProxyManager] Usando proxy: ${proxy.host}:${proxy.port}`);
return this.createAgent(proxy);
}
}
/**
* Instancia global del proxy manager.
* Lee SAT_PROXY_LIST y SAT_PROXY_STRATEGY del entorno.
*/
export const proxyManager = new ProxyManager();

View File

@@ -10,6 +10,7 @@ import {
DocumentStatus,
ServiceEndpoints,
} from '@nodecfdi/sat-ws-descarga-masiva';
import { proxyManager } from './proxy.service.js';
export interface FielData {
cerContent: string;
@@ -47,11 +48,20 @@ export function createSatService(fielData: FielData): Service {
}
// Crear cliente HTTP con timeout explícito para evitar el bug de la librería
// cuando ocurre un timeout de red.
// cuando ocurre un timeout de red. Si hay proxies configurados, se usa uno
// del pool para reducir el riesgo de bloqueo por IP del SAT.
const proxyAgent = proxyManager.createNextAgent();
if (proxyAgent) {
console.log('[SAT] Usando proxy para la conexión con el SAT');
} else {
console.log('[SAT] Sin proxy configurado; usando IP directa del servidor');
}
const webClient = new (HttpsWebClient as any)(
undefined,
undefined,
SAT_WEB_CLIENT_TIMEOUT_MS,
proxyAgent,
);
// Crear request builder con la FIEL

View File

@@ -1,5 +1,5 @@
diff --git a/build/index.js b/build/index.js
index bf7a6aafce966c4ab44ba3abb240578cc68779d6..4678df8734b098f20c7e14d8593bd4328fe759d8 100644
index bf7a6aafce966c4ab44ba3abb240578cc68779d6..df01102262bbe6d544a5a1b4453977be8a826d0d 100644
--- a/build/index.js
+++ b/build/index.js
@@ -266,7 +266,13 @@ var ServiceConsumer = class _ServiceConsumer {
@@ -17,3 +17,27 @@ index bf7a6aafce966c4ab44ba3abb240578cc68779d6..4678df8734b098f20c7e14d8593bd432
}
this.checkErrors(request, response, exception);
return response.getBody();
@@ -2660,10 +2666,12 @@ var HttpsWebClient = class {
_fireRequestClosure;
_fireResponseClosure;
_timeout;
- constructor(onFireRequest, onFireResponse, timeout) {
+ _agent;
+ constructor(onFireRequest, onFireResponse, timeout, agent = void 0) {
this._fireRequestClosure = onFireRequest;
this._fireResponseClosure = onFireResponse;
this._timeout = timeout;
+ this._agent = agent;
}
fireRequest(request) {
if (this._fireRequestClosure) {
@@ -2679,7 +2687,8 @@ var HttpsWebClient = class {
const options = {
method: request.getMethod(),
headers: request.getHeaders(),
- timeout: this._timeout ?? request.getTimeout() ?? void 0
+ timeout: this._timeout ?? request.getTimeout() ?? void 0,
+ agent: this._agent
};
return new Promise((resolve, reject) => {
let clientRequest;

6
pnpm-lock.yaml generated
View File

@@ -6,7 +6,7 @@ settings:
patchedDependencies:
'@nodecfdi/sat-ws-descarga-masiva@2.0.0':
hash: n2q5glw3wdhkcidljfdzrkmxnq
hash: i4ncoh7xgprkdron5l2ech4ifm
path: patches/@nodecfdi__sat-ws-descarga-masiva@2.0.0.patch
importers:
@@ -39,7 +39,7 @@ importers:
version: 3.2.0(luxon@3.7.2)
'@nodecfdi/sat-ws-descarga-masiva':
specifier: ^2.0.0
version: 2.0.0(patch_hash=n2q5glw3wdhkcidljfdzrkmxnq)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)
version: 2.0.0(patch_hash=i4ncoh7xgprkdron5l2ech4ifm)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)
'@prisma/client':
specifier: ^5.22.0
version: 5.22.0(prisma@5.22.0)
@@ -3133,7 +3133,7 @@ snapshots:
dependencies:
luxon: 3.7.2
'@nodecfdi/sat-ws-descarga-masiva@2.0.0(patch_hash=n2q5glw3wdhkcidljfdzrkmxnq)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)':
'@nodecfdi/sat-ws-descarga-masiva@2.0.0(patch_hash=i4ncoh7xgprkdron5l2ech4ifm)(@nodecfdi/cfdi-core@1.0.1)(luxon@3.7.2)':
dependencies:
'@nodecfdi/cfdi-core': 1.0.1
'@nodecfdi/credentials': 3.2.0(luxon@3.7.2)