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

@epistola.app/epistola-client

v1.4.0

Published

Node.js client for the Epistola document generation API, on the platform's own fetch. Generated from the Epistola OpenAPI contract, with identity headers, RFC 9457 problem-detail error handling, API-key and self-signed JWT auth, NDJSON result collection,

Readme

Epistola Node.js Client

Node.js client library for the Epistola document generation API, generated from the Epistola OpenAPI contract with OpenAPI Generator (typescript-fetch, on the platform's own fetch) and hand-written glue for identity, authentication, error handling, result collection, and client-side validation. TypeScript declarations are included.

It adds, on top of the stock generated client:

  • Client identity headers (User-Agent + X-EP-Node-Id) required on every request.
  • RFC 9457 problem-detail error handling — a typed ProblemDetailException with a typeSlug discriminator and generated KnownProblemSlugs constants.
  • Static API-key authentication via Authorization: ApiKey <key>, the supported method.
  • Self-signed JWT authentication (JwtSigner, experimental), minting a fresh short-lived token per request on node:crypto alone.
  • The Accept header each operation is declared with. The generated classes never set one, and Node's fetch sends */* by default — so without this a request never says it accepts application/problem+json, the document every error handler here is built to parse.
  • NDJSON result collection (ResultCollector) with adaptive polling, compression, and partition-aware routing helpers.
  • Client-side JSON Schema validation of template data (TemplateSchemaValidator) and generated validate<Model> helpers for the contract's own constraints.

The package version tracks the Epistola contract version (info.version) and releases in lockstep with the Kotlin, Jakarta EE, .NET and Python clients. It needs Node.js 22.12 or later and ships as an ES module (which Node's require() can load as well).

It has no runtime dependencies: HTTP is the platform's fetch, JWT signing is node:crypto, decompression is node:zlib. The one optional feature that needs a library is client-side template validation, which loads ajv and ajv-formats on first use; they are optional peer dependencies, so install them only if you use it (see Client-side validation).

Release history: CHANGELOG.md.

Install

npm install @epistola.app/epistola-client

Quick start

import { ClientIdentity, EpistolaClient, TemplatesApi } from '@epistola.app/epistola-client'

const identity = ClientIdentity.builder()
  .nodeId('my-pod-123')                       // defaults to the hostname
  .product('my-app', '1.0.0')                 // appended to User-Agent
  .build()

const client = EpistolaClient.builder('https://epistola.example.com/api')
  .identity(identity)                         // User-Agent + X-EP-Node-Id
  .apiKey('epk_...')                          // Authorization: ApiKey <key>
  .build()

const templates = new TemplatesApi(client)
const template = await templates.getTemplate({ tenantId: 'acme', catalogId: 'invoices', templateId: 'invoice' })

EpistolaClient.builder(baseUrl, apiKey) is a shorthand for the same thing. The legacy X-API-Key header is not sent: it remains supported server-side but is deprecated. Some Epistola Suite deployments disable API-key authentication entirely; switch on e.typeSlug === KnownProblemSlugs.API_KEY_AUTH_DISABLED and guide the caller to JWT auth.

Self-signed JWT authentication is experimental: Epistola Suite may not implement it yet, and the flow may still change. To try it, pass .jwtSigner(signer) instead of .apiKey(...):

import { JwtSigner } from '@epistola.app/epistola-client'

const signer = JwtSigner.builder()
  .consumerId('invoice-service')
  .privateKey(JwtSigner.loadPrivateKey('private.pem'))
  .build()

EpistolaClient is a generated Configuration, so every generated API class takes it directly. One builder can produce more than one client: call build() again after changing requestTimeoutMs(...) for the two profiles a long-running consumer typically needs against the same backend — no timeout (the default) for polling, rendering and large transfers, a bounded one for everything else.

Partial updates

The generated models are plain TypeScript interfaces, and the generated serializers drop a property you never set while sending one you set to null as null. On the API's PATCH operations, where the contract documents null as "clear this", that is exactly the distinction that matters: updateConsumer({ ..., updateConsumerRequest: { name: 'Billing' } }) renames the consumer and touches nothing else; { name: 'Billing', description: null } also clears the description.

Binary downloads

Every operation the contract declares as format: binary — downloadDocument, previewDocument, downloadImageContent — resolves to a Blob; Buffer.from(await blob.arrayBuffer()) gives you the bytes.

Uploading files

Multipart operations such as uploadImage and importCatalog take a Blob. Pass a File, which carries a name and a type:

import { readFile } from 'node:fs/promises'

const file = new File([await readFile('logo.png')], 'logo.png', { type: 'image/png' })
const image = await new ImagesApi(client).uploadImage({ tenantId: 'acme-corp', catalogId: 'main', file, name: 'Logo' })

A plain Blob is always sent with the filename blob, and as application/octet-stream unless it was given a type. The server then names the image blob.

Error handling

Catch ProblemDetailException and switch on typeSlug against KnownProblemSlugs. The slug list is open — the API can introduce new problem types without a client release — so always keep a default branch and fall back to the HTTP status for unrecognized types.

import { KnownProblemSlugs, ProblemDetailException, ResponseError, TenantsApi } from '@epistola.app/epistola-client'

try {
  await new TenantsApi(client).getTenant({ tenantId: 'acme' })
} catch (e) {
  if (e instanceof ProblemDetailException) {
    switch (e.typeSlug) {
      case KnownProblemSlugs.NOT_FOUND:
        log.warn(`tenant not found: ${e.detail}`)
        break
      case KnownProblemSlugs.FORBIDDEN:
        throw new PermissionError(e.detail)
      case KnownProblemSlugs.VALIDATION_ERROR:
        for (const err of e.errors) log.warn(`${err.field}: ${err.message}`)     // field-level ValidationProblemDetail
        break
      case KnownProblemSlugs.DATA_MODEL_VALIDATION_ERROR:
        for (const [example, failures] of Object.entries(e.validationErrors)) {  // 422 data-model failures
          for (const f of failures) log.warn(`${example} ${f.path}: ${f.message}`)
        }
        break
      default:
        throw e                                                                  // unknown / framework error — fall back to e.statusCode
    }
  } else if (e instanceof ResponseError) {
    // an error response without a problem document
  }
}

ProblemDetailException extends the generated ResponseError, so existing instanceof ResponseError sites keep working. It exposes type, typeSlug, title, problemStatus, detail, instance, errors, validationErrors, extensions (every member outside the RFC 9457 base five), statusCode, rawBody, isValidationProblem, and isDataModelValidationProblem.

Result collection

import { ResultCollector } from '@epistola.app/epistola-client'

const collector = ResultCollector.builder()
  .client(client)
  .tenantId('acme')
  .handler(async (result) => console.log(result.requestId, result.status))
  .build()

await collector.start()   // adaptive polling; resolves after collector.stop()

Results stream in one at a time and the batch is acknowledged only after every result in it was handled: a handler that throws leaves the batch unacknowledged, so it is redelivered. gzip is always decoded; zstd is offered and decoded where Node's zlib has it (22.15+). partitionFor, isMyPartition and routingKeyToMe compute, from the assignment the server reports, a routing key whose result comes back to this node.

Client-side validation

Validating template data against the template's JSON Schema needs Ajv, which is an optional peer dependency so that consumers who never validate do not carry a schema compiler:

npm install ajv ajv-formats

Without them, the first validate call rejects with an error saying exactly that. The generated validate<Model> helpers for the contract's own constraints need nothing extra.

import { TemplateSchemaValidator, ValidatingGenerationApi, validateCreateTenantRequest } from '@epistola.app/epistola-client'

// Data against the template's JSON Schema, fetched once and cached (TtlSchemaCache, 5 minutes).
const validator = new TemplateSchemaValidator(templatesApi)
await validator.validate('acme', 'invoices', 'invoice', data)     // throws TemplateDataValidationException

// Or transparently, before every generation call:
const generation = new ValidatingGenerationApi(new GenerationApi(client), templatesApi)

// A request model against the constraints the contract declares on it (slug patterns, lengths, ranges):
validateCreateTenantRequest({ id: 'Acme Corp', name: 'Acme' })  // throws ModelValidationException

Development

The client is generated from the bundled spec. From the repository root:

make bundle                       # produce openapi.yaml
cd contracts/api/clients/nodejs-fetch
pnpm install --frozen-lockfile
./generate.sh                     # stock client + derived sources into src/generated/
pnpm build                        # dist/
pnpm test                         # node:test, against the compiled tree

src/generated/ (the stock client under api/, and the derived contractVersion, contractIdentity, contractMediaTypes, contractOperations, knownProblemSlugs and modelValidation) is gitignored and rebuilt from the spec each time. The cross-client conformance suite drives this client with make conformance-node.