Entry points
dte-sdk is one package with four entry points. The split follows one rule: each entry states what it needs from the runtime. The root entry needs nothing but standard APIs, so it runs anywhere. The others need Node, and dte-sdk/pdf also needs two optional packages. This page says what each entry holds, how to import it and why they are separate.
The four entries#
| Import from | What it holds | Runs on |
|---|---|---|
dte-sdk | Documents, stamps, signatures, envelopes, books, the verification of received documents, the SII web services client over fetch and the public registry of authorized issuers | Node, Bun, Deno; bundles for the browser with no configuration |
dte-sdk/node | createNodeHttpsTransport, createDirectoryCapture, createDirectoryUploadArchive, createDirectoryTokenCache, createDirectoryFolioRegistry, findCafFiles, findLatestCafFile, resolveCafFile, cafFileScope | Node |
dte-sdk/portal | SII portal, folios and CAF requests, the folio registry, purchase and sales register, MIPYME backup, activity verification, and certification: sets, runner, exchange, upload of the samples and the receipts application | Node |
dte-sdk/pdf | Printed samples and the SII office of each commune (SII_OFFICES, siiOfficeForCommune) | Node, with pdf-lib and bwip-js installed |
DteError and isDteError come from dte-sdk and recognize what every entry throws.
Importing#
// Documents, stamps, signatures, envelopes, books and the SII web services client
import { issueDocument, createEnvelope, signEnvelope, createSiiClient, uploadEnvelope } from 'dte-sdk'
// Adapters that need Node: a custom HTTPS transport, files on disk, and the search for CAF files
import {
createNodeHttpsTransport,
createDirectoryCapture,
createDirectoryUploadArchive,
createDirectoryTokenCache,
resolveCafFile,
} from 'dte-sdk/node'
// The SII portal, folios, the register and certification
import { createPortalClient, queryFolioQuota, requestCaf } from 'dte-sdk/portal'
// Printed samples
import { renderPrintedSample } from 'dte-sdk/pdf'
// One error class for all of them
import { isDteError } from 'dte-sdk'The entries compose. A typical Node service that sends documents to the SII and keeps a record of what it sent imports from two of them:
import { createSiiClient } from 'dte-sdk'
import { createDirectoryCapture, createDirectoryUploadArchive } from 'dte-sdk/node'
const sii = createSiiClient({
environment: 'certification',
certificate,
companyRut: '76543210-3',
capture: createDirectoryCapture({ dir: './capture' }), // every HTTP exchange, redacted
archive: createDirectoryUploadArchive({ dir: './uploads' }), // the exact bytes of every upload
})The client itself is in the root entry; only the adapters that touch the file system or node:https are in dte-sdk/node.
The root entry: dte-sdk#
Everything that is computation or protocol lives here: the domain model, document creation and totals, CAF and certificate handling, the TED, XMLDSig, envelopes, the three SII books, the status and claim catalogs, and the SII web services client. The client talks over fetch by default, behind an injectable transport port. It also holds three things that need no certificate: the public registry of authorized issuers (queryAuthorizedIssuer), receiverAnswer, the pure helper that applies the eight-day rule of Ley 19.983 to the claim events of a document, and verifyReceivedDocuments, which checks the signature, the stamp and the CAF of the documents a supplier sends with the keys the SII signs every CAF with (SII_CAF_KEYS, with cafEnvironment naming the environment of each).
The root entry never imports a node: module. That has two consequences:
- It runs on Node, Bun and Deno, and CI imports every entry on Bun and Deno against the real build.
- It bundles for the browser with no configuration. esbuild, webpack, Vite and rollup all build a consumer of the root entry without polyfills or
externalsettings. That makes it usable in a build step, a worker or a front end that, for example, computes totals or validates a document.
That a module bundles for the browser does not make a browser the right place to stamp documents. Stamping needs the CAF private key and signing needs the certificate private key; both are secrets that belong on a server you control.
dte-sdk/node: the Node adapters#
These are the adapters the root cannot hold because they need Node:
createNodeHttpsTransport(): an HTTP transport overnode:https. Use it for a custom certificate authority (tls.ca, added to the public roots bundled with Node, not in place of them) or, only if you must, the legacy TLS settings. TLS verification stays on by default.createDirectoryCapture({ dir }): writes every HTTP exchange to a directory, redacted (tokens, seeds, cookies, private keys), for SII support tickets.createDirectoryUploadArchive({ dir }): keeps the exact bytes of every upload the SII answered, with its track id and the SII's reply.createDirectoryTokenCache({ dir }): a token cache on disk, for thetokenCacheof the SII client and thesessionCacheof the portal client, so a restart reuses the token or the session. One owner-only file per key; see Security.createDirectoryFolioRegistry({ dir }): a folio registry on disk (the in-memory one is indte-sdk/portal).findCafFiles,findLatestCafFileandresolveCafFile: search a directory for the CAF files of a type and company, newest first, and pick the one that still has free folios;cafFileScopegives the registry scope of a file, the oneresolveCafFilereturns. Seedte-sdk/node.
They load their node: modules lazily, on first use, never at import. Importing dte-sdk/node evaluates no node: module, which is also checked in tests, so the import itself is safe wherever it runs.
dte-sdk/portal: the SII portal#
The certificate-authenticated website of the SII, read as HTML, plus everything built on top of it: company data, folio quota, CAF requests, annulment and re-obtaining, the folio registry, the purchase and sales register, the MIPYME backup, the activity verification, and the certification sets and runner (including the exchange, the printed samples upload and the receipts application). It needs Node, because fetch cannot present a client certificate. See SII portal and folios and Certification.
dte-sdk/pdf: printed samples#
The PDF of each document following the SII manual, with its PDF417 stamp. It draws with pdf-lib and bwip-js, which are optional peer dependencies: they stay out of your install unless you ask for them, and they load on first use.
npm install pdf-lib bwip-jsWithout them, the render functions throw a DteError with code DEPENDENCY_UNAVAILABLE, and details.module names the package that could not be loaded:
import { isDteError } from 'dte-sdk'
import { renderPrintedSample, parsePrintableDocuments } from 'dte-sdk/pdf'
try {
const [doc] = parsePrintableDocuments(signed.xml)
const pdf = await renderPrintedSample(doc)
} catch (error) {
if (isDteError(error) && error.code === 'DEPENDENCY_UNAVAILABLE') {
console.error(`Install ${String(error.details.module)} to print samples`)
}
throw error
}It also holds the SII office of each commune, which the header prints: SII_OFFICES and siiOfficeForCommune.
The batch functions of this entry read and write files, which is one more reason it is Node only. Only dte-sdk/pdf names the optional peers: nothing else in the package loads them. See Printed samples.
Why they are split#
- The root entry bundles for the browser with no configuration. With the Node adapters inside it, esbuild and webpack failed on the dynamic
import('node:…'), whatever you imported. Now esbuild, webpack, Vite and rollup all build it. - One
DteErrorfor all of them. The build shares code between entries as ES module chunks, and a shared chunk keeps one copy of the error class. What any entry throws therefore passesinstanceofandisDteErrorfrom the root, which is checked on Node, Bun and Deno. - A smaller install.
pdf-libandbwip-jswere most of the package's installed size. Onlydte-sdk/pdfnames them, so they can be optional peers without breaking anyone else's bundler. - Each entry states what it needs. The root no longer offers Node-only functions to a project that targets a browser.
A consumer with a bundler gets about the same bytes either way: tree-shaking already dropped what it did not import, and node-forge, a dependency, is most of the rest.
ES modules only#
The package ships ES modules, no CommonJS build. It needs Node ≥ 22.12, the version from which require() can load an ES module. A CommonJS project loads every entry with require('dte-sdk'), require('dte-sdk/node') and so on, and gets the same module an import gets.
Jest in its default CommonJS mode cannot require() an ES module. Run Jest in ESM mode, or let it transform this package with transformIgnorePatterns.
For TypeScript, the subpath entries resolve under "moduleResolution": "nodenext", "bundler" and "node10": the package declares exports and typesVersions. See Installation.
Size budgets#
Bundle size is a budget enforced in CI by size-limit. Each limit is minified plus brotli, for the entry and the chunks it reaches, with the package's dependencies left out (node-forge, @xmldom/xmldom, fast-xml-parser, rut.ts, pdf-lib, bwip-js). The last column is what size-limit measured for the 0.8.0 build:
| Entry | Budget (min + brotli) | Measured in 0.8.0 |
|---|---|---|
dte-sdk | 36 kB | 34.33 kB |
dte-sdk/node | 8 kB | 7.09 kB |
dte-sdk/portal | 76 kB | 74.90 kB |
dte-sdk/pdf | 11 kB | 10.93 kB |
A build that grows past a budget fails CI, and raising a budget is a deliberate decision. The verification of received documents took dte-sdk from 31.54 kB in 0.3.0 to 33.87 kB in 0.4.0, over its 32 kB budget, which went to 34 kB. The fixes of 0.7.0 took it to 34.36 kB, over its 34 kB budget, which went to 36 kB: the check that a CAF's two keys are a pair, its character references read as the DOM reads them, the depth every DOM is allowed and the codResp of the receiver's answer. The catalog of the register's document codes took dte-sdk/portal from 72.28 kB in 0.1.0 to 72.99 kB in 0.2.0, the activity verification to 73.82 kB in 0.3.0, the stricter checks of 0.4.0 (the signature, and the <CAF> block parseCaf keeps) to 73.88 kB, and returning the CAFs a batch request already holds, with its check of the quantity, to 73.95 kB in 0.5.0, still under its budget. Returning what the other folio flows already hold when a later step fails (the annulment's tally, the CAFs a re-obtaining downloaded, the memory of an exact request that annulled), with the original error logged, took it to 74.33 kB in 0.6.0, over its 74 kB budget, which went to 76 kB. The stricter parseCaf, which the search for CAF files runs, took dte-sdk/node from 6.34 kB to 6.39 kB in 0.4.0, and the folio registry written for its owner only to 6.41 kB in 0.5.0. Reading a CAF file by its XML declaration took it to 7.09 kB in 0.7.0, with dte-sdk/portal at 74.90 kB and dte-sdk/pdf at 10.93 kB, all under their budgets. Reading a document paid in cash, with the registry's code 27, left dte-sdk at 34.33 kB in 0.8.0, and the other entries did not move. Since the dependencies are excluded, what lands in your bundle is larger: node-forge is the biggest part.
What guards the split#
The public API of each entry is listed in a test (tests/public-api.test.ts) so a function cannot move between entries by accident. The smoke tests over the built package check that loading an entry evaluates no node: module, that the root reaches none, that only dte-sdk/pdf names the optional peers, that errors cross entries, that the root bundles for the browser, and that consumers on nodenext, bundler and node10 resolution get real types.