Running on Deno and Edge Runtimes
ksef-client-ts runs on Deno-based runtimes — including Supabase Edge Functions — from v0.8.0 onward. This page covers setup, what is and isn't available under Deno, and how to consume the library the same way a Node user would.
Overview
The library targets Node.js as its primary runtime, but its public surface (authentication, session encryption, invoice send/query, QR codes, offline mode, Zod-based validation) uses only APIs that exist in both Node and Deno's Node compatibility layer. A single native dependency — libxmljs2, used only for optional XSD validation — is the one feature that does not cross over.
Three runtime-specific fixes landed across v0.7.2 and v0.8.0 to close the gaps that previously prevented the library from running on Deno: preserving the node: prefix on built-in imports in the bundled output, SPKI extraction before the RSA-OAEP call, and the RSA-OAEP call itself moving from node:crypto.publicEncrypt (whose hash option Deno silently ignores) to Web Crypto (crypto.subtle.encrypt).
Installation
Deno resolves npm packages natively via the npm: specifier — no lockfile, no package.json, no npm install required. Import the library directly:
import { KSeFClient } from 'npm:ksef-client-ts@0.8.0';
const client = new KSeFClient({ environment: 'TEST' });
const challenge = await client.auth.getChallenge();
console.log(challenge.timestampMs);Run it:
deno run --allow-net --allow-read --allow-env script.tsDeno downloads the tarball and its transitive dependencies on first run and caches them at ~/Library/Caches/deno/npm/ (or the equivalent path on Linux/Windows). Subsequent runs are offline.
If you prefer pinning via a deno.json:
{
"imports": {
"ksef-client-ts": "npm:ksef-client-ts@0.8.0"
}
}Then:
import { KSeFClient } from 'ksef-client-ts';Using a local node_modules/ (optional)
When you already have a node_modules/ populated by npm / yarn / pnpm (for example, in a polyglot project where the same codebase runs on both Node and Deno), tell Deno to resolve packages from it:
{
"nodeModulesDir": "manual"
}This is the mode Supabase Edge Functions uses under the hood.
What works
Everything the library exposes, with one exception listed in the next section:
| Feature | Works on Deno |
|---|---|
Authentication (loginWithToken, loginWithCertificate, loginWithPkcs12, external-signature flow) | ✅ |
| Session management (online and batch) | ✅ |
| Invoice send / query / download | ✅ |
| UPO (receipt) fetching and parsing | ✅ |
| Permission management | ✅ |
| Certificate enrollment | ✅ |
| QR code generation (PNG, SVG) | ✅ |
| Invoice XML serialization (FA2, FA3, PEF, PEF_KOR) | ✅ |
| Offline mode (in-memory storage) | ✅ |
Zod-based invoice validation (validate(), validateSchema()) | ✅ |
| HTTP retry, rate-limit, and circuit-breaker policies | ✅ |
All cryptographic operations — RSA-OAEP token encryption, AES-256-CBC session encryption, ECDH+AES-GCM alternative, CSR and self-signed certificate generation — use the standard node:crypto and Web Crypto APIs and work identically on Deno.
What doesn't work: validateAgainstXsd
The one API that does not work on Deno is validateAgainstXsd(xml, xsdPath), used for strict XSD-level invoice validation against the official KSeF schemas. It depends on libxmljs2, which wraps the native libxml2 C library via N-API. Deno's Node compatibility layer cannot load that binding (any attempt to require('libxmljs2') or import('libxmljs2') will segfault the runtime — an upstream Deno limitation, not something fixable in the library).
What happens if you call it anyway
Assuming libxmljs2 is not installed (the expected state on Deno — you should not install it, see below), the call throws a clear, catchable error:
try {
validateAgainstXsd(xml, xsdPath);
} catch (err) {
// Error: libxmljs2 is not installed; cannot run XSD validation.
// Install it as an optional peer dependency (e.g. `yarn add -O libxmljs2` or `npm i -O libxmljs2`).
}The library exports a helper to identify this specific failure so you can fall back gracefully:
import { validateAgainstXsd, validate, isMissingLibxmljsError } from 'npm:ksef-client-ts@0.8.0';
try {
const result = validateAgainstXsd(xml, xsdPath);
// ...
} catch (err) {
if (isMissingLibxmljsError(err)) {
// Fall back to Zod-based schema validation — pure JS, works on Deno.
const result = await validate(xml);
// ...
} else {
throw err;
}
}Zod validation is usually enough
For the common case — "build an invoice XML, verify it's structurally correct before sending" — Zod-based validation (validate(), validateSchema()) is sufficient. It checks every required field, format (NIP, PESEL, KSeF numbers), enum values, multi-rate VAT group constraints, and business rules end-to-end. XSD validation adds pedantic structural checks against the official schema; it's useful for debugging hard-to-explain server-side rejections, not for routine validation.
Why you should not install libxmljs2 on Deno
If libxmljs2 somehow ends up in a node_modules/ that Deno can see (for example, a shared folder with a Node project), the segfault happens at library import time, not at call time. Your whole app fails to start with a segmentation fault rather than a catchable error.
The library marks libxmljs2 as an optional peer dependency, so a plain deno add npm:ksef-client-ts or npm install ksef-client-ts will never pull it in transitively. Only an explicit direct install will. On a Deno-only setup, there is no reason to do that.
Supabase Edge Functions
Edge Functions use Deno 1.x with the Node compatibility layer enabled and resolve npm packages from a per-function node_modules/ generated at deploy time. Nothing in this library requires special configuration for that environment.
Example function
// supabase/functions/ksef-check/index.ts
import { KSeFClient } from 'npm:ksef-client-ts@0.8.0';
Deno.serve(async (_req) => {
const nip = Deno.env.get('KSEF_NIP');
const token = Deno.env.get('KSEF_TOKEN');
if (!nip || !token) {
return new Response('missing credentials', { status: 500 });
}
const client = new KSeFClient({ environment: 'PROD' });
await client.loginWithToken(token, nip);
const grants = await client.permissions.queryPersonalGrants();
return Response.json({ permissions: grants.permissions });
});Deploy:
supabase functions deploy ksef-checkSet secrets:
supabase secrets set KSEF_NIP=1234567890 KSEF_TOKEN=...Storage considerations
Edge Function file systems are ephemeral and read-only on the function path. The library's offline-mode file storage (FileOfflineInvoiceStorage, which writes to ~/.ksef/offline/) does not work there — use InMemoryOfflineInvoiceStorage instead, or back offline state with a database/object store of your own.
Troubleshooting
Import "fast-xml-parser" not a dependency
Deno cannot find the npm dependency. Either:
- Use the full
npm:prefix in your import:import { KSeFClient } from 'npm:ksef-client-ts' - Add a
deno.jsonwith{ "nodeModulesDir": "manual" }and populatenode_modules/vianpm/yarnfirst
Segfault during import
Almost always means libxmljs2 is present in a node_modules/ directory that Deno is resolving. Remove it:
rm -rf node_modules/libxmljs2If you're on a dev machine that runs both Node (with libxmljs2 for XSD validation) and Deno (for Edge Functions testing), keep them in separate project directories with separate node_modules/.
HTTP 450 on loginWithToken under Supabase Edge Functions
If you see status.details: ["Invalid token encryption."] on auth status, you're on a pre-0.8.0 version. Earlier versions passed OAEP parameters through node:crypto.publicEncrypt, which Deno's Node-compat layer silently ignored — Deno emitted OAEP-SHA1 ciphertext that KSeF rejects, regardless of what the library requested. 0.8.0+ uses Web Crypto natively (crypto.subtle.encrypt with {name: 'RSA-OAEP', hash: 'SHA-256'}), which is runtime-portable by construction.
Check the installed version — the library's package.json isn't part of its exports map, so import 'npm:ksef-client-ts/package.json' won't resolve under Deno. Use Deno's tooling instead:
# See what the npm: specifier resolves to right now
deno info npm:ksef-client-ts
# Or, if you're using a lockfile, grep for the pinned version
grep -E '"npm:ksef-client-ts@[^"]+"' deno.lockThe npm registry page lists every published version. You need 0.8.0 or later for Deno support; on older versions, no client-side workaround exists — publicEncrypt on Deno cannot be forced to emit OAEP-SHA256.