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

@weiliang79/ubl-builder

v0.4.1

Published

Build OASIS UBL 2.1 documents as XML or UBL JSON, with country profiles — Malaysia MyInvois included

Readme

ubl-builder

license: MIT

Build XML documents to the OASIS UBL 2.1 (Universal Business Language) standard, with optional country profiles layered on top.

  • Every UBL 2.1 aggregate component — all 109, held to the OASIS schemas by CI checks that fail on any disagreement in element name, sequence position, cardinality or the class a value is built into
  • XML or JSON — the same document renders as XML or as OASIS UBL JSON v2.0, the format Malaysia's MyInvois accepts alongside XML
  • Country profiles — the core knows only UBL; jurisdictions add their vocabulary and derived values behind a small interface

UBL 2.1 documentation: https://docs.oasis-open.org/ubl/os-UBL-2.1/UBL-2.1.html UBL 2.1 schema reference: https://www.datypic.com/sc/ubl21/ss.html

Install

npm install @weiliang79/ubl-builder

Requires Node 20 or newer. Digests are computed with Web Crypto through the crypto global, which Node exposes by default only from version 19.

Usage

import { Invoice } from '@weiliang79/ubl-builder/documents';
import { myInvois } from '@weiliang79/ubl-builder/profiles/myinvois';

const invoice = new Invoice('INV-0001');

myInvois.defaults(invoice); // namespace declarations for the profile
invoice.setIssueDate('2026-07-02').setIssueTime('02:02:36Z').setDocumentCurrencyCode('MYR');

console.log(invoice.getXml(true));
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Invoice
    xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
    xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
    xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
  <cbc:ID>INV-0001</cbc:ID>
  <cbc:IssueDate>2026-07-02</cbc:IssueDate>
  <cbc:IssueTime>02:02:36Z</cbc:IssueTime>
  <cbc:DocumentCurrencyCode>MYR</cbc:DocumentCurrencyCode>
</Invoice>

The namespace declarations are wrapped here for readability; the real output puts them on the root element's own line.

For submission, getXml(false, true) gives the headless single-line form whose bytes the MyInvois documentHash is computed over. getJson() renders the same document as UBL JSON.

Reading a document back

Invoice.fromXml and Invoice.fromJson are the inverses. Both rebuild the document from the same tables that write one, so a document read back renders byte for byte as it arrived:

const invoice = Invoice.fromXml(receivedXml);
invoice.setDueDate('2026-08-01');
console.log(invoice.getXml(false, true));

One caveat for hashing: an empty element written <x></x> is re-serialised as <x/>, so a documentHash taken over a re-serialised third-party document may not match the sender's. Hash the bytes you received.

An element this library cannot represent throws rather than being dropped — 32 UBL children still have no component class, and losing one in transit is worse than refusing the document. The UBL JSON form carries only the four namespaces it hoists to _D / _A / _B / _E, so a document that reaches you as JSON has whatever others it once had already gone.

Entry points

The root import re-exports everything. Subpaths are narrower:

| Import | Contains | | ------------------------- | ------------------------------------------------------------------ | | @weiliang79/ubl-builder | everything | | .../documents | Invoice | | .../cac | aggregate components — Party, TaxTotal, InvoiceLine, … | | .../datatypes | UdtAmount, UdtCode, UBLVersionID, the CCT and XSD primitives | | .../ext | UBLExtension, UBLExtensions | | .../sig | UBLDocumentSignatures, SignatureInformation | | .../profiles/myinvois | Malaysia — LHDN vocabulary and profile | | .../profiles/dian | Colombia — retained, unmaintained | | .../core | the component base, serializers and node types |

Schemas

The OASIS UBL 2.1 OS schemas are vendored under schemas/ and are the source of truth for every component's structure. They are development-only and are not published.

npm run scaffold          # write a component file for a schema type that has none
npm run report:schema     # differences between the components and the schemas
npm run check:schema      # fail if any params map disagrees (runs in CI)
npm run check:types       # fail if a component disagrees with its params map, or is unexported
npm run check:classref    # fail if a classRef is not the class implementing that element's type
npm run validate:xsd      # validate the golden fixtures against the XSD
npm run check:c14n        # fail if our Canonical XML 1.1 disagrees with libxml2

Validation is structural only — element sequence, names, cardinality, datatypes. Not EN 16931 business rules: MyInvois deviates from BR-CO-13 by reporting line-level discounts in AllowanceTotalAmount while LineExtensionAmount is already net of them, so business-rule validation rejects conformant documents.

Signing — MyInvois document version 1.1

Version 1.0 is accepted unsigned. Version 1.1 enables signature validation and needs an XAdES-BES signature, which withSigner attaches:

import { myInvois } from '@weiliang79/ubl-builder/profiles/myinvois';

const signing = myInvois.withSigner({
  sign: (bytes) => myHsm.sign(bytes), // RSA-SHA256 over the bytes given
  certificate: { base64, issuerName, serialNumber },
});

signing.defaults(invoice);
// …build the invoice…
await signing.finalize(invoice); // bumps listVersionID to 1.1, then signs

The private key never enters this library — sign is a callback, so a file, a smartcard, an HSM or a cloud KMS all work, and the package still runs in a browser. issuerName is supplied rather than parsed because the order of the relative names is not canonical and differs between CAs; LHDN wants RFC 4514 order, which is the reverse of OpenSSL's rendering.

Submit the compact rendering — getXml(), never getXml(true). LHDN recanonicalizes what it receives, and a standard canonicalizer counts indentation between elements as content.

Three of the four values in the signature reproduce LHDN's own published signed reference document exactly; see test/profiles/lhdnSignedSample.spec.ts for what is proven and what is not. No submission has yet been accepted, so treat 1.1 as unverified against the live service.

Signing needs a certificate from an MCMC-licensed CA — an organisational one, and self-signed certificates are rejected in the sandbox as well as in production.

Validation — MyInvois rules checked offline

myInvois.finalize checks the document against the MyInvois rules that are decidable without contacting LHDN, and throws MyInvoisValidationError with every issue it found:

import { myInvois, MyInvoisValidationError } from '@weiliang79/ubl-builder/profiles/myinvois';

myInvois.defaults(invoice);
// …build the invoice…
try {
  myInvois.finalize(invoice);
} catch (error) {
  if (error instanceof MyInvoisValidationError) {
    error.issues.forEach((issue) => console.error(issue.code, issue.path, issue.message));
  }
}

myInvois.validate(invoice) answers the same question without throwing:

const { valid, issues } = myInvois.validate(invoice);

The result is shaped like LHDN's own response — a verdict alongside the detail — so the offline check and the API's answer can be handled by the same code.

Three classes of rejection are covered, all of which otherwise cost a round trip to LHDN:

  • A missing required element (MYI004). UBL marks most of MyInvois's mandatory fields optional, so the type system cannot help: a document with no issue date, no tax total, or a party with no address compiles and serialises happily. This also catches the library's one silent failure — a plain object passed where a component instance is required emits an empty element rather than raising, and an empty element has no value to find.
  • A dropped attribute (MYI001). Scalar params accept a bare string as shorthand for the Udt* classes, and a bare string carries no attributes — so taxAmount: '0.00' emits an amount with no currencyID. That is schema-valid and rejected on submission. The same applies to schemeID, listID and listVersionID.
  • An incoherent consolidated e-Invoice (MYI003). Using the General Public TIN as the buyer silently reclassifies the document, and every line's classification code must change with it.

The rule of admission is that a check must be decidable from the document alone and must never reject a document LHDN accepts — a validator that blocks valid invoices is worse than none, since the only workaround is to stop using it. 0.4.0 also required buyer state code 17 on a consolidated document. That was a false positive and is removed in 0.4.1 — LHDN accepts state code 14 there. The rule came from a real rejection whose submission differed from the accepted one in two places at once, and the wrong one was credited. A rule now has to come from a submission that changed a single thing.

A presence rule is admitted only if the element appears in both documents this repo knows LHDN accepted — the production fixture and LHDN's own published signed sample — which is how cbc:ElectronicMail was ruled out. Appearing in both is still not proof of being required, so cbc:PostalZone, cbc:TaxCurrencyCode, cbc:InvoicedQuantity and its unitCode are excluded too: LHDN models each as an optional field in its own right.

Only the fields every Invoice type needs are checked. Credit, debit and refund notes also require cac:BillingReference naming the original, and the self-billed types (11–14) swap which party carries what; neither is checked, because this project has submitted neither. Anything needing LHDN's own state (whether a TIN exists, whether it matches your credentials, whether a submission duplicates an earlier one) is left to the API. Monetary totals are deliberately not checked; see the BR-CO-13 note above.

To skip validation, do not call finalize — at version 1.0 it is the only thing the hook does. When signing, validation runs before the signature is attached; call signInvoice directly to bypass it.

Not implemented

  • Profile constraints beyond the offline rules above. Anything needing LHDN's own state — whether a TIN exists, whether it matches the credentials behind a submission, whether a referenced document exists — is left to the API.

The library never computes or validates monetary totals — see the BR-CO-13 note above for why.

Upgrading

See MIGRATION.md. 0.1.0 moves every import path.

Credits

A fork of pipesanta/ubl-builder by Felipe Santa, with contributions from Lars Buur. The component model, the params-map interpreter and the original UBL type coverage came from there. MIT, as is this.