Compare commits

...

5 Commits

Author SHA1 Message Date
Consultoria AS
a64aa11548 feat(sat): add FIEL management and encryption services (Phase 2)
- Add sat-crypto.service.ts with AES-256-GCM encryption for secure
  credential storage using JWT_SECRET as key derivation source
- Add fiel.service.ts with complete FIEL lifecycle management:
  - Upload and validate FIEL credentials (.cer/.key files)
  - Verify certificate is FIEL (not CSD) and not expired
  - Store encrypted credentials in database
  - Retrieve and decrypt credentials for SAT sync operations
- Install @nodecfdi/credentials for FIEL/CSD handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:44:04 +00:00
Consultoria AS
787aac9a4c feat(sat): add database models for SAT sync
Phase 1 - Database models:
- Add FielCredential model for encrypted FIEL storage
- Add SatSyncJob model for sync job tracking
- Add SAT-related enums (SatSyncType, SatSyncStatus, CfdiSyncType)
- Add TypeScript types in shared package
- Relations: Tenant -> FielCredential (1:1), Tenant -> SatSyncJobs (1:N)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:38:51 +00:00
Consultoria AS
3763014eca docs: add SAT sync implementation plan
Detailed implementation plan with 9 phases:
1. Database models and migrations
2. Cryptography and FIEL services
3. SAT communication services
4. CFDI XML parser
5. Main orchestrator service
6. Scheduled cron job
7. API endpoints
8. Frontend components
9. Testing and validation

Includes:
- 16 new files to create
- 5 files to modify
- Dependencies list
- Implementation order

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:36:53 +00:00
Consultoria AS
b49902bcff docs: add SAT sync feature design
Design document for automatic CFDI synchronization with SAT:
- FIEL (e.firma) authentication
- Download emitted and received CFDIs
- Daily automated sync at 3:00 AM
- Initial extraction of last 10 years
- Encrypted credential storage (AES-256-GCM)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:35:12 +00:00
Consultoria AS
519de61c6f chore: add systemd service files for auto-start
Add systemd unit files for automatic service startup:
- horux-api.service: API server on port 4000
- horux-web.service: Web frontend on port 3000

Services are configured to:
- Start automatically on boot
- Restart on failure
- Depend on PostgreSQL

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:17:47 +00:00
11 changed files with 1216 additions and 1 deletions

View File

@@ -15,6 +15,7 @@
}, },
"dependencies": { "dependencies": {
"@horux/shared": "workspace:*", "@horux/shared": "workspace:*",
"@nodecfdi/credentials": "^3.2.0",
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -23,6 +24,7 @@
"express": "^4.21.0", "express": "^4.21.0",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"node-forge": "^1.3.3",
"zod": "^3.23.0" "zod": "^3.23.0"
}, },
"devDependencies": { "devDependencies": {
@@ -31,6 +33,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.7", "@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/node-forge": "^1.3.14",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"tsx": "^4.19.0", "tsx": "^4.19.0",
"typescript": "^5.3.0" "typescript": "^5.3.0"

View File

@@ -20,6 +20,8 @@ model Tenant {
expiresAt DateTime? @map("expires_at") expiresAt DateTime? @map("expires_at")
users User[] users User[]
fielCredential FielCredential?
satSyncJobs SatSyncJob[]
@@map("tenants") @@map("tenants")
} }
@@ -62,3 +64,75 @@ enum Role {
contador contador
visor visor
} }
// ============================================
// SAT Sync Models
// ============================================
model FielCredential {
id String @id @default(uuid())
tenantId String @unique @map("tenant_id")
rfc String @db.VarChar(13)
cerData Bytes @map("cer_data")
keyData Bytes @map("key_data")
keyPasswordEncrypted Bytes @map("key_password_encrypted")
encryptionIv Bytes @map("encryption_iv")
encryptionTag Bytes @map("encryption_tag")
serialNumber String? @map("serial_number") @db.VarChar(50)
validFrom DateTime @map("valid_from")
validUntil DateTime @map("valid_until")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@map("fiel_credentials")
}
model SatSyncJob {
id String @id @default(uuid())
tenantId String @map("tenant_id")
type SatSyncType
status SatSyncStatus @default(pending)
dateFrom DateTime @map("date_from") @db.Date
dateTo DateTime @map("date_to") @db.Date
cfdiType CfdiSyncType? @map("cfdi_type")
satRequestId String? @map("sat_request_id") @db.VarChar(50)
satPackageIds String[] @map("sat_package_ids")
cfdisFound Int @default(0) @map("cfdis_found")
cfdisDownloaded Int @default(0) @map("cfdis_downloaded")
cfdisInserted Int @default(0) @map("cfdis_inserted")
cfdisUpdated Int @default(0) @map("cfdis_updated")
progressPercent Int @default(0) @map("progress_percent")
errorMessage String? @map("error_message")
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
retryCount Int @default(0) @map("retry_count")
nextRetryAt DateTime? @map("next_retry_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([status])
@@index([status, nextRetryAt])
@@map("sat_sync_jobs")
}
enum SatSyncType {
initial
daily
}
enum SatSyncStatus {
pending
running
completed
failed
}
enum CfdiSyncType {
emitidos
recibidos
}

View File

@@ -0,0 +1,232 @@
import { Credential } from '@nodecfdi/credentials/node';
import { prisma } from '../config/database.js';
import { encrypt, decrypt } from './sat/sat-crypto.service.js';
import type { FielStatus } from '@horux/shared';
/**
* Sube y valida credenciales FIEL
*/
export async function uploadFiel(
tenantId: string,
cerBase64: string,
keyBase64: string,
password: string
): Promise<{ success: boolean; message: string; status?: FielStatus }> {
try {
// Decodificar archivos de Base64
const cerData = Buffer.from(cerBase64, 'base64');
const keyData = Buffer.from(keyBase64, 'base64');
// Validar que los archivos sean válidos y coincidan
let credential: Credential;
try {
credential = Credential.create(
cerData.toString('binary'),
keyData.toString('binary'),
password
);
} catch (error: any) {
return {
success: false,
message: 'Los archivos de la FIEL no son válidos o la contraseña es incorrecta',
};
}
// Verificar que sea una FIEL (no CSD)
if (!credential.isFiel()) {
return {
success: false,
message: 'El certificado proporcionado no es una FIEL (e.firma). Parece ser un CSD.',
};
}
// Obtener información del certificado
const certificate = credential.certificate();
const rfc = certificate.rfc();
const serialNumber = certificate.serialNumber().bytes();
const validFrom = certificate.validFromDateTime();
const validUntil = certificate.validToDateTime();
// Verificar que no esté vencida
if (new Date() > validUntil) {
return {
success: false,
message: 'La FIEL está vencida desde ' + validUntil.toLocaleDateString(),
};
}
// Encriptar credenciales
const { encrypted: encryptedCer, iv, tag } = encrypt(cerData);
const { encrypted: encryptedKey } = encrypt(keyData);
const { encrypted: encryptedPassword } = encrypt(Buffer.from(password, 'utf-8'));
// Guardar o actualizar en BD
await prisma.fielCredential.upsert({
where: { tenantId },
create: {
tenantId,
rfc,
cerData: encryptedCer,
keyData: encryptedKey,
keyPasswordEncrypted: encryptedPassword,
encryptionIv: iv,
encryptionTag: tag,
serialNumber,
validFrom,
validUntil,
isActive: true,
},
update: {
rfc,
cerData: encryptedCer,
keyData: encryptedKey,
keyPasswordEncrypted: encryptedPassword,
encryptionIv: iv,
encryptionTag: tag,
serialNumber,
validFrom,
validUntil,
isActive: true,
updatedAt: new Date(),
},
});
const daysUntilExpiration = Math.ceil(
(validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
return {
success: true,
message: 'FIEL configurada correctamente',
status: {
configured: true,
rfc,
serialNumber,
validFrom: validFrom.toISOString(),
validUntil: validUntil.toISOString(),
isExpired: false,
daysUntilExpiration,
},
};
} catch (error: any) {
console.error('[FIEL Upload Error]', error);
return {
success: false,
message: error.message || 'Error al procesar la FIEL',
};
}
}
/**
* Obtiene el estado de la FIEL de un tenant
*/
export async function getFielStatus(tenantId: string): Promise<FielStatus> {
const fiel = await prisma.fielCredential.findUnique({
where: { tenantId },
select: {
rfc: true,
serialNumber: true,
validFrom: true,
validUntil: true,
isActive: true,
},
});
if (!fiel || !fiel.isActive) {
return { configured: false };
}
const now = new Date();
const isExpired = now > fiel.validUntil;
const daysUntilExpiration = Math.ceil(
(fiel.validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
);
return {
configured: true,
rfc: fiel.rfc,
serialNumber: fiel.serialNumber || undefined,
validFrom: fiel.validFrom.toISOString(),
validUntil: fiel.validUntil.toISOString(),
isExpired,
daysUntilExpiration: isExpired ? 0 : daysUntilExpiration,
};
}
/**
* Elimina la FIEL de un tenant
*/
export async function deleteFiel(tenantId: string): Promise<boolean> {
try {
await prisma.fielCredential.delete({
where: { tenantId },
});
return true;
} catch {
return false;
}
}
/**
* Obtiene las credenciales desencriptadas para usar en sincronización
* Solo debe usarse internamente por el servicio de SAT
*/
export async function getDecryptedFiel(tenantId: string): Promise<{
credential: Credential;
rfc: string;
} | null> {
const fiel = await prisma.fielCredential.findUnique({
where: { tenantId },
});
if (!fiel || !fiel.isActive) {
return null;
}
// Verificar que no esté vencida
if (new Date() > fiel.validUntil) {
return null;
}
try {
// Desencriptar
const cerData = decrypt(
Buffer.from(fiel.cerData),
Buffer.from(fiel.encryptionIv),
Buffer.from(fiel.encryptionTag)
);
const keyData = decrypt(
Buffer.from(fiel.keyData),
Buffer.from(fiel.encryptionIv),
Buffer.from(fiel.encryptionTag)
);
const password = decrypt(
Buffer.from(fiel.keyPasswordEncrypted),
Buffer.from(fiel.encryptionIv),
Buffer.from(fiel.encryptionTag)
).toString('utf-8');
// Crear credencial
const credential = Credential.create(
cerData.toString('binary'),
keyData.toString('binary'),
password
);
return {
credential,
rfc: fiel.rfc,
};
} catch (error) {
console.error('[FIEL Decrypt Error]', error);
return null;
}
}
/**
* Verifica si un tenant tiene FIEL configurada y válida
*/
export async function hasFielConfigured(tenantId: string): Promise<boolean> {
const status = await getFielStatus(tenantId);
return status.configured && !status.isExpired;
}

View File

@@ -0,0 +1,122 @@
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
import { env } from '../../config/env.js';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const TAG_LENGTH = 16;
/**
* Deriva una clave de 256 bits del JWT_SECRET
*/
function deriveKey(): Buffer {
return createHash('sha256').update(env.JWT_SECRET).digest();
}
/**
* Encripta datos usando AES-256-GCM
*/
export function encrypt(data: Buffer): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
const iv = randomBytes(IV_LENGTH);
const key = deriveKey();
const cipher = createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
const tag = cipher.getAuthTag();
return { encrypted, iv, tag };
}
/**
* Desencripta datos usando AES-256-GCM
*/
export function decrypt(encrypted: Buffer, iv: Buffer, tag: Buffer): Buffer {
const key = deriveKey();
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
}
/**
* Encripta un string y retorna los componentes
*/
export function encryptString(text: string): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
return encrypt(Buffer.from(text, 'utf-8'));
}
/**
* Desencripta a string
*/
export function decryptToString(encrypted: Buffer, iv: Buffer, tag: Buffer): string {
return decrypt(encrypted, iv, tag).toString('utf-8');
}
/**
* Encripta credenciales FIEL (cer, key, password)
*/
export function encryptFielCredentials(
cerData: Buffer,
keyData: Buffer,
password: string
): {
encryptedCer: Buffer;
encryptedKey: Buffer;
encryptedPassword: Buffer;
iv: Buffer;
tag: Buffer;
} {
// Usamos el mismo IV y tag para simplificar, concatenando los datos
const combined = Buffer.concat([
Buffer.from(cerData.length.toString().padStart(10, '0')),
cerData,
Buffer.from(keyData.length.toString().padStart(10, '0')),
keyData,
Buffer.from(password, 'utf-8'),
]);
const { encrypted, iv, tag } = encrypt(combined);
// Extraemos las partes encriptadas
const cerLength = cerData.length;
const keyLength = keyData.length;
const passwordLength = Buffer.from(password, 'utf-8').length;
return {
encryptedCer: encrypted.subarray(0, 10 + cerLength),
encryptedKey: encrypted.subarray(10 + cerLength, 20 + cerLength + keyLength),
encryptedPassword: encrypted.subarray(20 + cerLength + keyLength),
iv,
tag,
};
}
/**
* Desencripta credenciales FIEL
*/
export function decryptFielCredentials(
encryptedCer: Buffer,
encryptedKey: Buffer,
encryptedPassword: Buffer,
iv: Buffer,
tag: Buffer
): {
cerData: Buffer;
keyData: Buffer;
password: string;
} {
const combined = Buffer.concat([encryptedCer, encryptedKey, encryptedPassword]);
const decrypted = decrypt(combined, iv, tag);
// Parseamos las partes
const cerLengthStr = decrypted.subarray(0, 10).toString();
const cerLength = parseInt(cerLengthStr, 10);
const cerData = decrypted.subarray(10, 10 + cerLength);
const keyLengthStr = decrypted.subarray(10 + cerLength, 20 + cerLength).toString();
const keyLength = parseInt(keyLengthStr, 10);
const keyData = decrypted.subarray(20 + cerLength, 20 + cerLength + keyLength);
const password = decrypted.subarray(20 + cerLength + keyLength).toString('utf-8');
return { cerData, keyData, password };
}

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Horux360 API Server
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/root/Horux/apps/api
Environment=NODE_ENV=production
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
ExecStart=/root/.local/share/pnpm/pnpm dev
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,17 @@
[Unit]
Description=Horux360 Web Frontend
After=network.target horux-api.service
Wants=horux-api.service
[Service]
Type=simple
User=root
WorkingDirectory=/root/Horux/apps/web
Environment=NODE_ENV=production
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
ExecStart=/root/.local/share/pnpm/pnpm dev
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,327 @@
# Diseño: Sincronización con SAT
## Resumen
Implementar sincronización automática de CFDIs desde el portal del SAT usando la e.firma (FIEL).
## Requisitos
| Aspecto | Decisión |
|---------|----------|
| Autenticación | FIEL (archivos .cer y .key + contraseña) |
| Tipos de CFDI | Emitidos y recibidos |
| Ejecución | Programada diaria a las 3:00 AM |
| Almacenamiento credenciales | Encriptadas en PostgreSQL (AES-256-GCM) |
| Primera extracción | Últimos 10 años |
| Extracciones posteriores | Solo mes actual |
| Duplicados | Actualizar con versión del SAT |
---
## Arquitectura General
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Frontend │────▶│ API Horux │────▶│ SAT WSDL │
│ (Configuración)│ │ (sat.service) │ │ Web Service│
└─────────────────┘ └──────────────────┘ └─────────────┘
┌──────────────┐
│ PostgreSQL │
│ - fiel_credentials
│ - sat_sync_jobs
│ - cfdis
└──────────────┘
```
---
## Integración con Web Services del SAT
### Flujo de Descarga
```
1. AUTENTICACIÓN (Token válido por 5 minutos)
- Crear timestamp (Created + Expires)
- Generar digest SHA-1 del timestamp
- Firmar digest con llave privada (.key) usando RSA-SHA1
- Enviar SOAP con certificado (.cer) + firma
- Recibir token SAML para usar en siguientes llamadas
2. SOLICITUD DE DESCARGA
Parámetros:
- RfcSolicitante: RFC de la empresa
- FechaInicio: YYYY-MM-DDTHH:MM:SS
- FechaFin: YYYY-MM-DDTHH:MM:SS
- TipoSolicitud: "CFDI" o "Metadata"
- TipoComprobante: "I"(ingreso), "E"(egreso), "T", "N", "P"
- RfcEmisor / RfcReceptor: Filtrar por contraparte (opcional)
Respuesta:
- IdSolicitud: UUID para tracking
- CodEstatus: 5000 = Aceptada
3. VERIFICACIÓN (Polling cada 30-60 segundos)
Estados posibles:
- 1: Aceptada (en proceso)
- 2: En proceso
- 3: Terminada (lista para descargar)
- 4: Error
- 5: Rechazada
- 6: Vencida
Respuesta exitosa incluye:
- IdsPaquetes: Array de IDs de paquetes ZIP a descargar
- NumeroCFDIs: Total de comprobantes encontrados
4. DESCARGA DE PAQUETES
- Por cada IdPaquete, solicitar descarga
- Respuesta: Paquete en Base64 (archivo ZIP)
- Decodificar y extraer XMLs
- Cada ZIP puede contener hasta 200,000 CFDIs
5. PROCESAMIENTO DE XMLs
Por cada XML:
- Parsear con @nodecfdi/cfdi-core
- Extraer: UUID, emisor, receptor, total, impuestos, fecha
- Buscar en BD por UUID
- Si existe → UPDATE
- Si no existe → INSERT
- Guardar XML original
```
### Endpoints del SAT
| Servicio | URL |
|----------|-----|
| Autenticación | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/Autenticacion/Autenticacion.svc` |
| Solicitud | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc` |
| Verificación | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/VerificaSolicitudDescargaService.svc` |
| Descarga | `https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc` |
### Estructura SOAP para Autenticación
```xml
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>2026-01-25T00:00:00.000Z</u:Created>
<u:Expires>2026-01-25T00:05:00.000Z</u:Expires>
</u:Timestamp>
<o:BinarySecurityToken
ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
u:Id="uuid-cert">
<!-- Certificado .cer en Base64 -->
</o:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue><!-- SHA1 del Timestamp --></DigestValue>
</Reference>
</SignedInfo>
<SignatureValue><!-- Firma RSA-SHA1 --></SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference URI="#uuid-cert"/>
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body/>
</s:Envelope>
```
### Dependencias Node.js
```json
{
"@nodecfdi/credentials": "^2.0",
"@nodecfdi/cfdi-core": "^0.5",
"node-forge": "^1.3",
"fast-xml-parser": "^4.0",
"adm-zip": "^0.5",
"node-cron": "^3.0"
}
```
### Códigos de Error del SAT
| Código | Significado | Acción |
|--------|-------------|--------|
| 5000 | Solicitud recibida | Continuar con verificación |
| 5002 | Se agotó límite de solicitudes | Esperar 24 horas |
| 5004 | No se encontraron CFDIs | Registrar, no es error |
| 5005 | Solicitud duplicada | Usar IdSolicitud existente |
| 404 | Paquete no encontrado | Reintentar en 1 minuto |
| 500 | Error interno SAT | Reintentar con backoff |
### Estrategia de Extracción Inicial (10 años)
- Dividir en solicitudes mensuales (~121 solicitudes)
- Procesar 3-4 meses por día para no saturar
- Guardar progreso en sat_sync_jobs
- Si falla, continuar desde último mes exitoso
### Tiempos Estimados
| Operación | Tiempo |
|-----------|--------|
| Autenticación | 1-2 segundos |
| Solicitud aceptada | 1-2 segundos |
| Verificación (paquete listo) | 1-30 minutos |
| Descarga 10,000 CFDIs | 30-60 segundos |
| Procesamiento 10,000 XMLs | 2-5 minutos |
---
## Modelo de Datos
### Nuevas Tablas (schema public)
```sql
-- Credenciales FIEL por tenant (encriptadas)
CREATE TABLE fiel_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
rfc VARCHAR(13) NOT NULL,
cer_data BYTEA NOT NULL,
key_data BYTEA NOT NULL,
key_password_encrypted BYTEA NOT NULL,
serial_number VARCHAR(50),
valid_from TIMESTAMP NOT NULL,
valid_until TIMESTAMP NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id)
);
-- Jobs de sincronización
CREATE TABLE sat_sync_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL,
date_from DATE NOT NULL,
date_to DATE NOT NULL,
cfdi_type VARCHAR(10),
sat_request_id VARCHAR(50),
sat_package_ids TEXT[],
cfdis_found INTEGER DEFAULT 0,
cfdis_downloaded INTEGER DEFAULT 0,
cfdis_inserted INTEGER DEFAULT 0,
cfdis_updated INTEGER DEFAULT 0,
progress_percent INTEGER DEFAULT 0,
error_message TEXT,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
retry_count INTEGER DEFAULT 0,
next_retry_at TIMESTAMP
);
CREATE INDEX idx_sat_sync_jobs_tenant ON sat_sync_jobs(tenant_id);
CREATE INDEX idx_sat_sync_jobs_status ON sat_sync_jobs(status);
```
### Modificaciones a tabla cfdis
```sql
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'manual';
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS sat_sync_job_id UUID;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS xml_original TEXT;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS last_sat_sync TIMESTAMP;
```
---
## Estructura de Archivos
```
apps/api/src/
├── services/
│ ├── sat/
│ │ ├── sat.service.ts
│ │ ├── sat-auth.service.ts
│ │ ├── sat-download.service.ts
│ │ ├── sat-parser.service.ts
│ │ └── sat-crypto.service.ts
│ └── fiel.service.ts
├── controllers/
│ ├── sat.controller.ts
│ └── fiel.controller.ts
├── routes/
│ ├── sat.routes.ts
│ └── fiel.routes.ts
└── jobs/
└── sat-sync.job.ts
```
---
## API Endpoints
```
POST /api/fiel/upload # Subir .cer, .key y contraseña
GET /api/fiel/status # Estado de FIEL configurada
DELETE /api/fiel # Eliminar credenciales
POST /api/sat/sync # Sincronización manual
GET /api/sat/sync/status # Estado actual
GET /api/sat/sync/history # Historial
GET /api/sat/sync/:id # Detalle de job
POST /api/sat/sync/:id/retry # Reintentar job fallido
```
---
## Interfaz de Usuario
### Sección en Configuración
- Estado de FIEL (configurada/no configurada, vigencia)
- Botones: Actualizar FIEL, Eliminar
- Sincronización automática (frecuencia, última sync, total CFDIs)
- Botón: Sincronizar Ahora
- Historial de sincronizaciones (tabla)
### Modal de Carga FIEL
- Input para archivo .cer
- Input para archivo .key
- Input para contraseña
- Mensaje de seguridad
- Botones: Cancelar, Guardar y Validar
---
## Notificaciones
| Evento | Mensaje |
|--------|---------|
| Sync completada | "Se descargaron X CFDIs del SAT" |
| Sync fallida | "Error al sincronizar: [mensaje]" |
| FIEL por vencer (30 días) | "Tu e.firma vence el DD/MMM/YYYY" |
| FIEL vencida | "Tu e.firma ha vencido" |
---
## Seguridad
- Solo rol `admin` puede gestionar FIEL
- Credenciales nunca se devuelven en API
- Logs de auditoría para accesos
- Rate limiting en endpoints de sincronización
- Encriptación AES-256-GCM para credenciales

View File

@@ -0,0 +1,228 @@
# Plan de Implementación: Sincronización SAT
## Fase 1: Base de Datos y Modelos
### 1.1 Migraciones Prisma
- [ ] Agregar modelo `FielCredential` en schema.prisma
- [ ] Agregar modelo `SatSyncJob` en schema.prisma
- [ ] Agregar campos a modelo `Cfdi`: source, sat_sync_job_id, xml_original, last_sat_sync
- [ ] Ejecutar migración
### 1.2 Tipos TypeScript
- [ ] Crear `packages/shared/src/types/sat.ts` con interfaces
- [ ] Exportar tipos en index.ts
## Fase 2: Servicios de Criptografía y FIEL
### 2.1 Servicio de Criptografía
- [ ] Crear `apps/api/src/services/sat/sat-crypto.service.ts`
- [ ] Implementar encrypt() con AES-256-GCM
- [ ] Implementar decrypt()
- [ ] Tests unitarios
### 2.2 Servicio de FIEL
- [ ] Crear `apps/api/src/services/fiel.service.ts`
- [ ] uploadFiel() - validar y guardar credenciales encriptadas
- [ ] getFielStatus() - obtener estado sin exponer datos sensibles
- [ ] deleteFiel() - eliminar credenciales
- [ ] validateFiel() - verificar que .cer y .key coincidan
- [ ] isExpired() - verificar vigencia
### 2.3 Dependencias
- [ ] Instalar @nodecfdi/credentials
- [ ] Instalar node-forge
## Fase 3: Servicios de Comunicación SAT
### 3.1 Servicio de Autenticación SAT
- [ ] Crear `apps/api/src/services/sat/sat-auth.service.ts`
- [ ] buildAuthSoapEnvelope() - construir XML de autenticación
- [ ] signWithFiel() - firmar con llave privada
- [ ] getToken() - obtener token SAML del SAT
- [ ] Manejo de errores y reintentos
### 3.2 Servicio de Descarga SAT
- [ ] Crear `apps/api/src/services/sat/sat-download.service.ts`
- [ ] requestDownload() - solicitar descarga de CFDIs
- [ ] verifyRequest() - verificar estado de solicitud
- [ ] downloadPackage() - descargar paquete ZIP
- [ ] Polling con backoff exponencial
### 3.3 Dependencias
- [ ] Instalar fast-xml-parser
- [ ] Instalar adm-zip
## Fase 4: Procesamiento de CFDIs
### 4.1 Servicio de Parser
- [ ] Crear `apps/api/src/services/sat/sat-parser.service.ts`
- [ ] extractZip() - extraer XMLs del ZIP
- [ ] parseXml() - parsear XML a objeto
- [ ] mapToDbModel() - mapear a modelo de BD
### 4.2 Dependencias
- [ ] Instalar @nodecfdi/cfdi-core
## Fase 5: Orquestador Principal
### 5.1 Servicio Principal SAT
- [ ] Crear `apps/api/src/services/sat/sat.service.ts`
- [ ] startSync() - iniciar sincronización
- [ ] processInitialSync() - extracción de 10 años
- [ ] processDailySync() - extracción mensual
- [ ] saveProgress() - guardar progreso en sat_sync_jobs
- [ ] handleError() - manejo de errores y reintentos
## Fase 6: Job Programado
### 6.1 Cron Job
- [ ] Crear `apps/api/src/jobs/sat-sync.job.ts`
- [ ] Configurar ejecución a las 3:00 AM
- [ ] Obtener tenants con FIEL activa
- [ ] Ejecutar sync para cada tenant
- [ ] Logging y monitoreo
### 6.2 Dependencias
- [ ] Instalar node-cron
## Fase 7: API Endpoints
### 7.1 Controlador FIEL
- [ ] Crear `apps/api/src/controllers/fiel.controller.ts`
- [ ] POST /upload - subir credenciales
- [ ] GET /status - obtener estado
- [ ] DELETE / - eliminar credenciales
### 7.2 Controlador SAT
- [ ] Crear `apps/api/src/controllers/sat.controller.ts`
- [ ] POST /sync - iniciar sincronización manual
- [ ] GET /sync/status - estado actual
- [ ] GET /sync/history - historial
- [ ] GET /sync/:id - detalle de job
- [ ] POST /sync/:id/retry - reintentar
### 7.3 Rutas
- [ ] Crear `apps/api/src/routes/fiel.routes.ts`
- [ ] Crear `apps/api/src/routes/sat.routes.ts`
- [ ] Registrar en app.ts
## Fase 8: Frontend
### 8.1 Componentes
- [ ] Crear `apps/web/components/sat/FielUploadModal.tsx`
- [ ] Crear `apps/web/components/sat/SyncStatus.tsx`
- [ ] Crear `apps/web/components/sat/SyncHistory.tsx`
### 8.2 Página de Configuración
- [ ] Crear `apps/web/app/(dashboard)/configuracion/sat/page.tsx`
- [ ] Integrar componentes
- [ ] Conectar con API
### 8.3 API Client
- [ ] Agregar métodos en `apps/web/lib/api.ts`
- [ ] uploadFiel()
- [ ] getFielStatus()
- [ ] deleteFiel()
- [ ] startSync()
- [ ] getSyncStatus()
- [ ] getSyncHistory()
## Fase 9: Testing y Validación
### 9.1 Tests
- [ ] Tests unitarios para servicios de criptografía
- [ ] Tests unitarios para parser de XML
- [ ] Tests de integración para flujo completo
- [ ] Test con FIEL de prueba del SAT
### 9.2 Validación
- [ ] Probar carga de FIEL
- [ ] Probar sincronización manual
- [ ] Probar job programado
- [ ] Verificar CFDIs descargados
## Orden de Implementación
```
Fase 1 (BD)
Fase 2 (Crypto + FIEL)
Fase 3 (Auth + Download SAT)
Fase 4 (Parser)
Fase 5 (Orquestador)
Fase 6 (Cron Job)
Fase 7 (API)
Fase 8 (Frontend)
Fase 9 (Testing)
```
## Archivos a Crear/Modificar
### Nuevos Archivos (16)
```
apps/api/src/services/sat/sat-crypto.service.ts
apps/api/src/services/sat/sat-auth.service.ts
apps/api/src/services/sat/sat-download.service.ts
apps/api/src/services/sat/sat-parser.service.ts
apps/api/src/services/sat/sat.service.ts
apps/api/src/services/fiel.service.ts
apps/api/src/controllers/fiel.controller.ts
apps/api/src/controllers/sat.controller.ts
apps/api/src/routes/fiel.routes.ts
apps/api/src/routes/sat.routes.ts
apps/api/src/jobs/sat-sync.job.ts
packages/shared/src/types/sat.ts
apps/web/components/sat/FielUploadModal.tsx
apps/web/components/sat/SyncStatus.tsx
apps/web/components/sat/SyncHistory.tsx
apps/web/app/(dashboard)/configuracion/sat/page.tsx
```
### Archivos a Modificar (5)
```
apps/api/prisma/schema.prisma
apps/api/src/app.ts
apps/api/src/index.ts
packages/shared/src/index.ts
apps/web/lib/api.ts
```
## Dependencias a Instalar
```bash
# En apps/api
pnpm add @nodecfdi/credentials @nodecfdi/cfdi-core node-forge fast-xml-parser adm-zip node-cron
# Tipos
pnpm add -D @types/node-forge @types/node-cron
```
## Estimación por Fase
| Fase | Descripción | Complejidad |
|------|-------------|-------------|
| 1 | Base de datos | Baja |
| 2 | Crypto + FIEL | Media |
| 3 | Comunicación SAT | Alta |
| 4 | Parser | Media |
| 5 | Orquestador | Alta |
| 6 | Cron Job | Baja |
| 7 | API | Media |
| 8 | Frontend | Media |
| 9 | Testing | Media |

View File

@@ -8,6 +8,7 @@ export * from './types/impuestos';
export * from './types/alertas'; export * from './types/alertas';
export * from './types/reportes'; export * from './types/reportes';
export * from './types/calendario'; export * from './types/calendario';
export * from './types/sat';
// Constants // Constants
export * from './constants/plans'; export * from './constants/plans';

View File

@@ -0,0 +1,132 @@
// ============================================
// FIEL (e.firma) Types
// ============================================
export interface FielUploadRequest {
cerFile: string; // Base64
keyFile: string; // Base64
password: string;
}
export interface FielStatus {
configured: boolean;
rfc?: string;
serialNumber?: string;
validFrom?: string;
validUntil?: string;
isExpired?: boolean;
daysUntilExpiration?: number;
}
// ============================================
// SAT Sync Types
// ============================================
export type SatSyncType = 'initial' | 'daily';
export type SatSyncStatus = 'pending' | 'running' | 'completed' | 'failed';
export type CfdiSyncType = 'emitidos' | 'recibidos';
export interface SatSyncJob {
id: string;
tenantId: string;
type: SatSyncType;
status: SatSyncStatus;
dateFrom: string;
dateTo: string;
cfdiType?: CfdiSyncType;
satRequestId?: string;
satPackageIds: string[];
cfdisFound: number;
cfdisDownloaded: number;
cfdisInserted: number;
cfdisUpdated: number;
progressPercent: number;
errorMessage?: string;
startedAt?: string;
completedAt?: string;
createdAt: string;
retryCount: number;
}
export interface SatSyncStatusResponse {
hasActiveSync: boolean;
currentJob?: SatSyncJob;
lastCompletedJob?: SatSyncJob;
totalCfdisSynced: number;
}
export interface SatSyncHistoryResponse {
jobs: SatSyncJob[];
total: number;
page: number;
limit: number;
}
export interface StartSyncRequest {
type?: SatSyncType;
dateFrom?: string;
dateTo?: string;
}
export interface StartSyncResponse {
jobId: string;
message: string;
}
// ============================================
// SAT Web Service Types
// ============================================
export interface SatAuthResponse {
token: string;
expiresAt: Date;
}
export interface SatDownloadRequest {
rfcSolicitante: string;
fechaInicio: Date;
fechaFin: Date;
tipoSolicitud: 'CFDI' | 'Metadata';
tipoComprobante?: 'I' | 'E' | 'T' | 'N' | 'P';
rfcEmisor?: string;
rfcReceptor?: string;
}
export interface SatDownloadRequestResponse {
idSolicitud: string;
codEstatus: string;
mensaje: string;
}
export interface SatVerifyResponse {
codEstatus: string;
estadoSolicitud: number; // 1=Aceptada, 2=EnProceso, 3=Terminada, 4=Error, 5=Rechazada, 6=Vencida
codigoEstadoSolicitud: string;
numeroCfdis: number;
mensaje: string;
paquetes: string[];
}
export interface SatPackageResponse {
paquete: string; // Base64 ZIP
}
// ============================================
// SAT Error Codes
// ============================================
export const SAT_STATUS_CODES: Record<string, string> = {
'5000': 'Solicitud recibida con éxito',
'5002': 'Se agotó el límite de solicitudes',
'5004': 'No se encontraron CFDIs',
'5005': 'Solicitud duplicada',
};
export const SAT_REQUEST_STATUS: Record<number, string> = {
1: 'Aceptada',
2: 'En proceso',
3: 'Terminada',
4: 'Error',
5: 'Rechazada',
6: 'Vencida',
};

62
pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
'@horux/shared': '@horux/shared':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/shared version: link:../../packages/shared
'@nodecfdi/credentials':
specifier: ^3.2.0
version: 3.2.0(luxon@3.7.2)
'@prisma/client': '@prisma/client':
specifier: ^5.22.0 specifier: ^5.22.0
version: 5.22.0(prisma@5.22.0) version: 5.22.0(prisma@5.22.0)
@@ -44,6 +47,9 @@ importers:
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.3 version: 9.0.3
node-forge:
specifier: ^1.3.3
version: 1.3.3
zod: zod:
specifier: ^3.23.0 specifier: ^3.23.0
version: 3.25.76 version: 3.25.76
@@ -63,6 +69,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.0.0 specifier: ^22.0.0
version: 22.19.7 version: 22.19.7
'@types/node-forge':
specifier: ^1.3.14
version: 1.3.14
prisma: prisma:
specifier: ^5.22.0 specifier: ^5.22.0
version: 5.22.0 version: 5.22.0
@@ -439,6 +448,20 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@nodecfdi/base-converter@1.0.7':
resolution: {integrity: sha512-YoWtdhCPB86W+2TpXrZ1yXzehNC2sEFCB0vw4XtnHKdtw6pKxKyDT2qQf4TqICROp0IZNNKunFDw3EhcoR41Tw==}
engines: {node: '>=18 <=22 || ^16'}
'@nodecfdi/credentials@3.2.0':
resolution: {integrity: sha512-knZE8kIrIib27M/tcUQRgvnObMd7oR9EKZTSdBSHXW/5Pw6UB23v0ruUAJSFY0789J3OLfKaIVRXBG2I+q9ZTA==}
engines: {node: '>=18 <=22 || ^16'}
peerDependencies:
'@types/luxon': 3.4.2
luxon: ^3.5.0
peerDependenciesMeta:
'@types/luxon':
optional: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -989,6 +1012,9 @@ packages:
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node-forge@1.3.14':
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
'@types/node@14.18.63': '@types/node@14.18.63':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
@@ -1018,6 +1044,10 @@ packages:
'@types/serve-static@2.2.0': '@types/serve-static@2.2.0':
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
'@vilic/node-forge@1.3.2-5':
resolution: {integrity: sha512-8GVr3S/nmLKL7QI7RYhVIcz3PuT/fxfkQLuh/F1CaT+/3QgI14RqiJkcKIni7h9u4ySbQGiGvm4XbNxRBJin4g==}
engines: {node: '>= 6.13.0'}
accepts@1.3.8: accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -1663,6 +1693,10 @@ packages:
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1749,6 +1783,10 @@ packages:
sass: sass:
optional: true optional: true
node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'}
node-releases@2.0.27: node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
@@ -2140,6 +2178,9 @@ packages:
ts-interface-checker@0.1.13: ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-mixer@6.0.4:
resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==}
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -2444,6 +2485,15 @@ snapshots:
'@next/swc-win32-x64-msvc@14.2.33': '@next/swc-win32-x64-msvc@14.2.33':
optional: true optional: true
'@nodecfdi/base-converter@1.0.7': {}
'@nodecfdi/credentials@3.2.0(luxon@3.7.2)':
dependencies:
'@nodecfdi/base-converter': 1.0.7
'@vilic/node-forge': 1.3.2-5
luxon: 3.7.2
ts-mixer: 6.0.4
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -2988,6 +3038,10 @@ snapshots:
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 22.19.7
'@types/node@14.18.63': {} '@types/node@14.18.63': {}
'@types/node@22.19.7': '@types/node@22.19.7':
@@ -3018,6 +3072,8 @@ snapshots:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 22.19.7 '@types/node': 22.19.7
'@vilic/node-forge@1.3.2-5': {}
accepts@1.3.8: accepts@1.3.8:
dependencies: dependencies:
mime-types: 2.1.35 mime-types: 2.1.35
@@ -3717,6 +3773,8 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
luxon@3.7.2: {}
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
media-typer@0.3.0: {} media-typer@0.3.0: {}
@@ -3793,6 +3851,8 @@ snapshots:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
node-forge@1.3.3: {}
node-releases@2.0.27: {} node-releases@2.0.27: {}
normalize-path@3.0.0: {} normalize-path@3.0.0: {}
@@ -4207,6 +4267,8 @@ snapshots:
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
ts-mixer@6.0.4: {}
tslib@2.8.1: {} tslib@2.8.1: {}
tsx@4.21.0: tsx@4.21.0: