Skip to content

JavaScript / TypeScript bindings

The fatoority ES module package runs the Rust SDK in WebAssembly. Diplomat generates the object bindings and TypeScript declarations; a small facade provides async fetch calls. Node 22+ and modern browsers are supported. Browser execution requires WebAssembly, Web Crypto, FinalizationRegistry and top-level await.

Install

Install the package from npm:

npm install fatoority

Build from source

From a source checkout with Rust, Node 22+, npm and Python 3:

rustup target add wasm32-unknown-unknown
npm ci --prefix bindings/js
python3 scripts/build_js.py
(cd bindings/js && npm pack)

Install the resulting tarball in your application:

npm install /path/to/fatoority-0.4.2.tgz

The generated bindings are checked in. Regeneration requires diplomat-tool 0.16.1; follow the binding workflow when changing Rust bridge declarations. Version tags publish the tested package to npm through GitHub Actions.

import { Config, SigningKey, Xml } from 'fatoority';

const config = Config.new_(0); // 0: nonproduction, 1: simulation, 2: production
const key = SigningKey.generate();
const report = JSON.parse(Xml.validateZatca(config, invoiceXml));
if (!report.is_valid) {
  console.log(report.stages);
}

Declarations are included and work with TypeScript NodeNext and bundler module resolution. No separate @types package is needed.

Licensing and build flags

The WASM includes SDK-derived rule predicates and messages under the upstream terms documented in third-party notices, matching native distributions. The npm package is not entirely MIT/Apache-2.0; its license field points to its bundled notice. See the package README's licensing explanation for the LGPL/GPL texts, source, and the recorded CEN provenance limitation.

User-set RUSTFLAGS or CARGO_ENCODED_RUSTFLAGS override .cargo/config.toml; retain --cfg getrandom_backend="custom" when building WASM with either variable.

API requests

import { Config as ApiConfig, CsidProduction, SignedInvoice, ZatcaClient } from 'fatoority';

export async function reportInvoice(signedXml, token, secret, { signal, fetch: transport = globalThis.fetch } = {}) {
  const client = new ZatcaClient(ApiConfig.new_(2), { fetch: transport });
  const credentials = CsidProduction.create(2, null, token, secret);
  const invoice = SignedInvoice.fromXml(signedXml);
  const response = await client.reportSimplifiedInvoice(invoice, credentials, false, 'en', { signal });
  response.ensureAccepted();
  return response;
}

The configuration and credentials must use the same environment. API methods prepare requests synchronously in Rust, await the selected fetch transport, then decode status/body bytes in Rust. Outgoing bytes are copied before the network wait. HTTP success does not establish invoice acceptance; inspect outcome() or call ensureAccepted().

Method Credential scope / result
postCsrForCcsid(csr, otp, options?) Issues compliance credentials
postCcsidForPcsid(credentials, options?) Compliance credentials to production credentials
renewCsid(credentials, csr, otp, language?, options?) Renews production credentials
checkInvoiceCompliance(invoice, credentials, options?) Compliance credentials; validation response
reportSimplifiedInvoice(invoice, credentials, clearanceStatus?, language?, options?) Production credentials; reporting response
clearStandardInvoice(invoice, credentials, clearanceStatus?, language?, options?) Production credentials; clearance response

options.signal accepts an AbortSignal. Inject a transport through new ZatcaClient(config, { fetch }). The default adapter refuses redirects and passes HTTP error responses to Rust decoding.

For a custom HTTP client, import ZatcaClient from fatoority/raw and call its prepare* methods. The returned request owns method(), url(), headersJson() (header pairs), body().asSlice(), and the operation/environment needed by decode(status, bytes). sendPrepared(request, { fetch, signal }) is the standard adapter. Prepared headers contain credentials; avoid logging them.

Values, ownership and errors

  • Decimal inputs and outputs are strings, preserving exact amounts. Invoice counters are bigint.
  • Optional strings use null; an empty string stays distinct. Generated method names use camelCase, and Rust new constructors become new_.
  • build(), signing and intoXml() consume inner invoice state. Reuse fails. Generated objects are garbage-collected through FinalizationRegistry; pointers are internal and cannot be shared between workers.
  • Generated byte results are copies. Each worker needs its own WASM instance and objects.
  • A Rust failure throws an Error whose cause is a BindingError. Read cause.code(), cause.message() and cause.detailsJson().
  • Fetch, cancellation and body-read failures throw TransportError with the original cause and optional httpStatus.

Xml.validateZatca returns JSON containing stage coverage and is_valid. Rejection and missing context must be distinguished from execution failure. Local validation does not establish gateway acceptance or certificate issuer trust.

The facade supplies one JS timestamp when evaluated_at is omitted. Set it explicitly in the options JSON for reproducible validation. The raw WASM API requires that field. Read files in Node or the browser and pass their contents; native filesystem helpers are excluded.

Browser deployment

The loader resolves fatoora_ffi.wasm relative to its package modules. Deploy the generated modules and WASM together, or configure your bundler to copy the WASM to that URL. Serve them over HTTP(S); the browser must permit WebAssembly execution and provide Web Crypto.

Direct ZATCA requests depend on gateway CORS. A custom server-side transport can perform the HTTP request while Rust retains ownership of request construction and response decoding. Choose where to retain credentials and private keys according to your application's trust boundary.

Artifact size

Measured with the same SDK capabilities and all 15 UBL schemas included. Sizes below are decimal MB; gzip uses level 9.

Build WASM Gzip
Original Rust release artifact 8.974 MB 2.010 MB
Previous package after host-glue cleanup 7.976 MB 1.862 MB
Size profile and wasm-opt -Oz 6.406 MB 1.510 MB
Compressed schemas and unused metadata removed 3.525 MB 1.512 MB

The wasm-release Cargo profile uses opt-level = "z", LTO, one codegen unit, symbol stripping and aborting panics. The build invokes pinned Binaryen 123's wasm-opt -Oz. It then removes unused wasm-bindgen descriptor metadata and checks the result against 4,000,000 bytes raw and 1,800,000 bytes gzipped. An oversized artifact fails the build.

The baseline contained 4.26 MB of code, 3.90 MB of data and 0.66 MB of function names. The XSD sources alone occupied 2,996,782 bytes. WASM embeds losslessly compressed resources; the build decompresses each resource and asserts byte equality with its original source. Validation inflates resources when compiling the cached schema. Original XSD files and their constraints are preserved.

Baseline function-name attribution found about 1.31 MB of XSD engine code and 0.80 MB of regex engine code. Those are approximate code-body totals, excluding shared generics and data tables. Unicode regex features remain enabled because schema and XML validation depend on their semantics. The final artifact contains about 2.46 MB of code and 1.04 MB of data.

Internal schema compression primarily reduces the raw WASM footprint; after gzip it is roughly neutral because the schema text was already highly compressible. Schema compilation still needs the decompressed contents in memory.

Reproduce the measurements:

python3 scripts/build_js.py
python3 scripts/check_wasm_size.py --json

xsd-schema also enables Chrono's wasm-bindgen features transitively. The build removes unused descriptor exports and maps the retained UTF-8 throw helper to Diplomat's equivalent import. It rejects unsupported host imports, including a JS clock or object runtime.

Verification

npm run check --prefix bindings/js
node bindings/js/tests/runtime.test.mjs
npx --prefix bindings/js playwright install chromium
node bindings/js/tests/browser.mjs
python3 scripts/check_js_package.py

These checks cover the optimized artifact, optional Arabic strings, exact decimals, consumed objects, memory growth, schema acceptance/rejection, all six API operations, cancellation and redirects. The npm check installs a packed tarball in a fresh directory and loads its bundled WASM. Live gateway and browser CORS acceptance require separate testing.