Compare commits
5 Commits
4fd6f01303
...
a64aa11548
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a64aa11548 | ||
|
|
787aac9a4c | ||
|
|
3763014eca | ||
|
|
b49902bcff | ||
|
|
519de61c6f |
@@ -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"
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ model Tenant {
|
|||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
232
apps/api/src/services/fiel.service.ts
Normal file
232
apps/api/src/services/fiel.service.ts
Normal 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;
|
||||||
|
}
|
||||||
122
apps/api/src/services/sat/sat-crypto.service.ts
Normal file
122
apps/api/src/services/sat/sat-crypto.service.ts
Normal 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 };
|
||||||
|
}
|
||||||
17
deploy/systemd/horux-api.service
Normal file
17
deploy/systemd/horux-api.service
Normal 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
|
||||||
17
deploy/systemd/horux-web.service
Normal file
17
deploy/systemd/horux-web.service
Normal 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
|
||||||
327
docs/plans/2026-01-25-sat-sync-design.md
Normal file
327
docs/plans/2026-01-25-sat-sync-design.md
Normal 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
|
||||||
|
|
||||||
228
docs/plans/2026-01-25-sat-sync-implementation.md
Normal file
228
docs/plans/2026-01-25-sat-sync-implementation.md
Normal 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 |
|
||||||
|
|
||||||
@@ -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';
|
||||||
|
|||||||
132
packages/shared/src/types/sat.ts
Normal file
132
packages/shared/src/types/sat.ts
Normal 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
62
pnpm-lock.yaml
generated
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user