npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@munyaal/cfdi

v1.0.0

Published

Librería para crear y firmar xml para emitir cfdi

Readme

@munyaal/cfdi

Esta es una librería para crear y sellar archivos xml para un CFDi 4.0

Características

  • Construcción fácil del comprobante a través de objetos tomando de base la documentación del Anexo 20
  • Generación de cadena original
  • Sellado del comprobante haciendo uso del certificado CSD (.cer) y la llave privada (.key) del emisor
  • Guardar los documentos en el directorio de preferencia
  • Contrato de errores unificado y tipado (CfdiError + CfdiErrorCode)

Requisitos

Esta librería está pensada para ser usada en un entorno back end, dicho entorno debe contar con lo siguiente.

  • Instalación de OpenSSL 3.0.7
  • El XSLT para la cadena original (assets/xslt/cadenaoriginal_4_0.xslt) y su SEF precompilado (assets/xslt/cadenaoriginal_4_0.sef.json) ya vienen incluidos en este paquete y se resuelven por defecto. La ruta in-process usa el SEF (≈1000× más rápido que xslt3), pero conserva byte-a-byte la misma cadena original. Solo pasa paths.xsltCfdi40 a initializeCfdi si quieres usar un XSLT propio o una versión distinta — en ese caso, el helper caerá al camino xslt3 legacy.
  • Debes contar con los siguientes archivos del emisor
    • Certificado CSD (.cer)
    • Llave privada (.key)
    • Contraseña del CSD
    • Documentación

Instalación

npm i @munyaal/cfdi

Uso

El uso de esta librería se podría resumir en los siguientes pasos.

  • Configuración del servicio
  • Creación del comprobante
  • Sellar y generar xml

Configuración del servicio

import { initializeCfdi } from '@munyaal/cfdi';

// Servicio que sella y certifica (camino por defecto).
const service = initializeCfdi({
    certificate: {
        cerPath: '/ruta/al/certificado.cer',
        keyPath: '/ruta/al/archivo.key',
        password: 'contraseña del CSD',
    },
    // `behavior` y `paths` son opcionales: ambas opciones sellan y
    // certifican por defecto y la XSLT resuelve a
    // `assets/xslt/cadenaoriginal_4_0.xslt` cuando no se sobreescribe.
    behavior: { seal: true, certify: true },
    paths: {
        xsltCfdi40: 'assets/xslt/cadenaoriginal_4_0.xslt',
        outputDir: '/tmp/cfdi',
    },
});

// Servicio "solo quiero el XML, no firmar". No requiere certificado.
const unsignService = initializeCfdi({
    behavior: { seal: false, certify: false },
});

Creación del comprobante

El Comprobante y sus nodos hijos (Emisor, Receptor, Conceptos, Impuestos) se asignan después de new, no se pasan dentro del constructor: cada constructor solo recibe sus propios atributos, y los hijos se montan sobre la instancia ya creada (igual que en los casos de prueba de test/cases/).

import {
    Comprobante,
    ComprobanteEmisor,
    ComprobanteReceptor,
    ComprobanteConcepto,
    ComprobanteConceptoImpuestos,
    ComprobanteConceptoImpuestosTraslado,
    ComprobanteImpuestos,
    ComprobanteImpuestosTraslado,
    MonedaEnum,
    TipoComprobanteEnum,
    ExportacionEnum,
    RegimenFiscalEnum,
    UsoCfdiEnum,
    ObjetoImpEnum,
    ImpuestoEnum,
    TipoFactorEnum,
    FormaPagoEnum,
    MetodoPagoEnum,
} from '@munyaal/cfdi';

const comprobante = new Comprobante({
    Version: '4.0',
    Serie: 'A',
    Folio: 'FOL-1',
    Fecha: '2024-01-01T00:00:00',
    FormaPago: FormaPagoEnum.FP03, // Transferencia electrónica de fondos
    SubTotal: '100.00',
    Moneda: MonedaEnum.MXN,
    Total: '116.00',
    TipoDeComprobante: TipoComprobanteEnum.I,
    Exportacion: ExportacionEnum.E01,
    MetodoPago: MetodoPagoEnum.PUE,
    LugarExpedicion: '77725',
});

comprobante.Emisor = new ComprobanteEmisor({
    Rfc: 'XAXX010101000',
    Nombre: 'Emisor de Prueba',
    RegimenFiscal: RegimenFiscalEnum.RF616, // Sin obligaciones fiscales
});

comprobante.Receptor = new ComprobanteReceptor({
    Rfc: 'XAXX010101000',
    Nombre: 'Receptor de Prueba',
    DomicilioFiscalReceptor: '77725',
    RegimenFiscalReceptor: RegimenFiscalEnum.RF616, // Sin obligaciones fiscales
    UsoCFDI: UsoCfdiEnum.S01, // Sin efectos fiscales
});

const concepto = new ComprobanteConcepto({
    ClaveProdServ: '01010101',
    Cantidad: '1',
    ClaveUnidad: 'H87',
    Descripcion: 'Servicio de prueba',
    ValorUnitario: '100.00',
    Importe: '100.00',
    ObjetoImp: ObjetoImpEnum.OI02, // Sí objeto de impuesto
});

const conceptoImpuestos = new ComprobanteConceptoImpuestos();
conceptoImpuestos.Traslados.push(
    new ComprobanteConceptoImpuestosTraslado({
        Base: '100.00',
        Impuesto: ImpuestoEnum.I002, // IVA
        TipoFactor: TipoFactorEnum.Tasa,
        TasaOCuota: '0.160000',
        Importe: '16.00',
    }),
);
concepto.Impuestos = conceptoImpuestos;

comprobante.Conceptos.push(concepto);

// Total de impuestos trasladados a nivel comprobante.
const impuestos = new ComprobanteImpuestos({
    TotalImpuestosTrasladados: '16.00',
});
impuestos.Traslados.push(
    new ComprobanteImpuestosTraslado({
        Base: '100.00',
        Impuesto: ImpuestoEnum.I002, // IVA
        TipoFactor: TipoFactorEnum.Tasa,
        TasaOCuota: '0.160000',
        Importe: '16.00',
    }),
);
comprobante.Impuestos = impuestos;

Sellar y generar xml

try {
    const xml = await service.getXMLSellado(comprobante);
    // `xml` es la cadena del CFDI 4.0 firmada y certificada.
    // `saveXml` la persiste en disco y devuelve la ruta del archivo.
    const path = await service.saveXml(xml, 'mi-cfdi');
    console.log(`CFDi firmado en: ${path}`);
} catch (e) {
    // Ver la sección de manejo de errores más abajo.
    handleError(e);
}

Manejo de errores

Cada fallo que produce la librería es una instancia de CfdiError (exportada desde @munyaal/cfdi), con la forma pública:

| Campo | Tipo | Descripción | | -------------- | ------------------- | ------------------------------------------------------------------------------------------ | | code | CfdiErrorCode | Identificador estable (uno de los valores de CfdiErrorCode). Es el contrato público. | | process | string | Etiqueta humana de la operación que falló (p. ej. getCertificate, sellarComprobante). | | message | string | Descripción humana de lo que salió mal (texto libre, no contiene el código). | | suggestions | readonly string[] | Pistas accionables, en orden, para que el usuario sepa cómo resolver el problema. | | cause | unknown | Error original de execa / openssl / fs (ES2022 Error.cause). | | toJSON() | object | Devuelve exactamente { code, process, message, suggestions } para logs y telemetría. |

Ejemplo: try / catch con CfdiError y CfdiErrorCode

import { CfdiError, CfdiErrorCode, initializeCfdi } from '@munyaal/cfdi';

const service = initializeCfdi({
    certificate: {
        cerPath: '/ruta/al/certificado.cer',
        keyPath: '/ruta/al/archivo.key',
        password: 'contraseña del CSD',
    },
});

try {
    const xml = await service.getXMLSellado(comprobante);
    // ...uso de `xml`...
} catch (e) {
    if (e instanceof CfdiError) {
        // `error.code` es el contrato estable. Se puede comparar
        // contra el enum (recomendado) o contra el string numérico.
        switch (e.code) {
            case CfdiErrorCode.CertificateRead:       // 'ERROR: 001'
            case CfdiErrorCode.CadenaOriginal:        // 'ERROR: 002'
            case CfdiErrorCode.PrivateKeyRead:        // 'ERROR: 003'
            case CfdiErrorCode.AssetNotFound:         // 'ERROR: 004'
            case CfdiErrorCode.XmlRequiredAttribute:  // 'ERROR: 005'
            case CfdiErrorCode.XmlRequiredElement:    // 'ERROR: 006'
            case CfdiErrorCode.XmlWrite:              // 'ERROR: 007'
            case CfdiErrorCode.Config:                // 'ERROR: 008'
            case CfdiErrorCode.Seal:                  // 'ERROR: 009'
            case CfdiErrorCode.Certify:               // 'ERROR: 010'
            case CfdiErrorCode.XmlInvalidAttribute:   // 'ERROR: 011' (Phase 1 round 2)
                // Rama según el código estable
                break;
        }

        console.error(`[${e.code}] ${e.process}: ${e.message}`);
        for (const s of e.suggestions) {
            console.error(`  - ${s}`);
        }

        // `e.cause` es el error original de execa / openssl / fs
        // cuando aplica. Útil para diagnóstico avanzado.
        if (e.cause instanceof Error) {
            console.error('causa original:', e.cause);
        }

        // `toJSON()` devuelve exactamente los cuatro campos públicos,
        // sin `cause`, sin `name`, sin `stack`. Es la forma
        // recomendada para serializar a logs / telemetría.
        const payload = e.toJSON();
        telemetry.track('cfdi_error', payload);
    } else {
        // Cualquier otro error es ajeno a la librería.
        throw e;
    }
}

Tabla de códigos

Todos los valores de CfdiErrorCode siguen el formato ERROR: NNN. La columna Mensaje / resumen resume el contenido típico de error.message; la columna Sugerencias típicas resume las pistas más comunes en error.suggestions (cada llamada puede añadir otras específicas del contexto, p. ej. el path concreto que falló).

| Enum member | Código | process | Mensaje / resumen | Sugerencias típicas | | ---------------------------- | -------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | CfdiErrorCode.CertificateRead | ERROR: 001 | getCertificate | No se pudo obtener el certificado (.cer). | Verifica la instalación de openssl. Valida la existencia del certificado en <path>. | | CfdiErrorCode.CadenaOriginal | ERROR: 002 | getCadenaOriginal | No se pudo generar la cadena original (xslt3). | Valida la existencia del XML temporal. Valida la existencia del XSLT en <path>. Si xslt3 no está en PATH, instala sus dependencias o npm i -g xslt3. | | CfdiErrorCode.PrivateKeyRead | ERROR: 003 | getKey | No se pudo obtener la clave privada (.key). | Verifica la instalación de openssl. Valida la existencia de la clave privada en <path>. | | CfdiErrorCode.AssetNotFound | ERROR: 004 | resolveAssetPath | No se encontró el asset <input> (lista los candidatos probados). | Verifica que el archivo exista en alguna de las rutas. Pasa la ruta explícita al inicializar. | | CfdiErrorCode.XmlRequiredAttribute | ERROR: 005 | XMLAttributeModel.setSchema | El atributo "<key>" es requerido. | Proporciona el valor en la instancia. Verifica que la propiedad esté presente antes de serializar. | | CfdiErrorCode.XmlRequiredElement | ERROR: 006 | XMLChildModel.setSchema | El elemento "<key>" es requerido. | Proporciona el valor en la instancia. Verifica que la propiedad esté presente antes de serializar. | | CfdiErrorCode.XmlWrite | ERROR: 007 | saveXml | No se pudo escribir el XML en el directorio de salida (<outputDir>). | Verifica que el directorio exista y sea escribible. Revisa los permisos. Asegúrate de que el nombre no apunte a un directorio. | | CfdiErrorCode.Config | ERROR: 008 | assertCertificateConfigured | certificate is required when behavior.seal or behavior.certify is true. o certificate is incomplete: missing <campos>. | Pasa certificate: { cerPath, keyPath, password } a initializeCfdi. O bien, desactiva el sellado con behavior: { seal: false, certify: false }. | | CfdiErrorCode.Seal | ERROR: 009 | sellarComprobante | No se pudo sellar el comprobante. | Verifica que el certificado, la clave privada y la contraseña sean correctos. Verifica que el XSLT sea accesible. Consulta err.cause. | | CfdiErrorCode.Certify | ERROR: 010 | certificarComprobante | No se pudo certificar el comprobante. | Verifica que el archivo .cer exista y sea legible por openssl. Verifica la instalación de openssl. Consulta err.cause. | | CfdiErrorCode.XmlInvalidAttribute | ERROR: 011 | XMLAttributeModel.setSchema | El atributo "<key>" no cumple con la restricción estructural (<motivo>). | Verifica que <key> cumpla con el patrón del XSD. Usa un valor del catálogo SAT si aplica. Distinto de ERROR: 005: el valor existe pero es incorrecto. |

Contrato estable: error.code y toJSON()

  • error.code es el contrato estable. Todos los demás campos son descriptivos y pueden ajustarse sin que eso sea un breaking change. Compara siempre contra CfdiErrorCode.X (recomendado) o contra el string numérico ('ERROR: 001', etc.). El string numérico es el valor del enum, así que ambas formas son la misma expresión.
  • error.toJSON() devuelve exactamente { code, process, message, suggestions }, sin cause, sin name, sin stack. Es la forma recomendada para serializar a logs, telemetría o respuestas de API. cause se mantiene solo en proceso: inspección in-memory, no JSON.
  • error.message es texto humano libre. No se garantiza que contenga el código, ni que sea estable entre versiones. Úsalo solo para mostrar al usuario final, no para branchear.
  • La librería no imprime errores automáticamente. No se hace ninguna llamada a console.error desde el código de la librería en ninguna ruta de fallo. El logging es decisión y responsabilidad del consumidor; si quieres persistir los errores, hazlo en tu catch a partir de e.toJSON() o de los campos públicos.

Ejemplo: serializar un error para una API de logs

import { CfdiError } from '@munyaal/cfdi';

try {
    await service.getXMLSellado(comprobante);
} catch (e) {
    if (e instanceof CfdiError) {
        // `e.toJSON()` es seguro de pasar a `JSON.stringify`: no
        // contiene `cause`, `name` ni `stack`, solo los cuatro
        // campos públicos.
        const payload = e.toJSON();
        // payload === {
        //   code: 'ERROR: 008',
        //   process: 'assertCertificateConfigured',
        //   message: 'certificate is required ...',
        //   suggestions: [
        //     'Pasa certificate: { cerPath, keyPath, password } a initializeCfdi',
        //     'O bien, desactiva el sellado con behavior: { seal: false, certify: false }',
        //   ],
        // }
        res.status(400).json({ ok: false, error: payload });
    } else {
        throw e;
    }
}

Migración desde versiones anteriores

Si vienes de una versión que usaba los códigos textuales ('ERROR: CONFIG', 'ERROR: AL SELLAR', 'ERROR: AL CERTIFICAR'), actualiza a la forma numérica ('ERROR: 008', 'ERROR: 009', 'ERROR: 010') o, mejor aún, al enum (CfdiErrorCode.Config, CfdiErrorCode.Seal, CfdiErrorCode.Certify). El resto del contrato (code / process / message / suggestions / cause / toJSON()) es estable desde la iteración 7.