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 Rustnewconstructors becomenew_. build(), signing andintoXml()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
Errorwhosecauseis aBindingError. Readcause.code(),cause.message()andcause.detailsJson(). - Fetch, cancellation and body-read failures throw
TransportErrorwith the originalcauseand optionalhttpStatus.
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.