Skip to content

Node.js package

This page has the details of @polyjose/node that you don’t need to get started: which systems the prebuilt addons support, how the package picks and checks the addon it loads, and what to do with secret results. To install it and sign your first token, see Get started with Node.js.

@polyjose/node is one package. From 0.1.2 it contains the prebuilt addons for all eight supported platforms, as polyjose.<platform>.node files, so npm downloads one tarball and nothing else, and nothing is compiled on your machine. At run time the package loads the one for your platform. There are addons for:

  • macOS: x64 and arm64 (darwin-x64, darwin-arm64)
  • Linux with glibc: x64 and arm64 (linux-x64-gnu, linux-arm64-gnu)
  • Linux with musl (Alpine): x64 and arm64 (linux-x64-musl, linux-arm64-musl)
  • Windows: x64 and arm64 (win32-x64-msvc, win32-arm64-msvc)

The same installed package works on all of them, so a node_modules built on one platform can be copied to another. On any other platform (FreeBSD, 32-bit, RISC-V and so on) the import throws ERR_POLYJOSE_ADDON_REFUSED, naming your platform; use @polyjose/wasm there, which has the same API.

Up to 0.1.1, each addon was a separate package, @polyjose/node-<platform>, installed as an optional dependency. Those packages are deprecated and are no longer used: @polyjose/node 0.1.2 and later never loads them, even if they are still installed.

The package is ESM only and needs Node.js 22.12 or later. CommonJS code can load it with require("@polyjose/node"), through Node’s require(esm), which needs no flag from 22.12 on. Every operation returns a Promise, and the work runs on the libuv thread pool, so it doesn’t block the event loop. The API and its TypeScript types are the same as @polyjose/wasm’s, so you can switch between the two packages by changing the import.

The Linux glibc addons (linux-x64-gnu, linux-arm64-gnu) need glibc 2.28 or later. That is also the floor of the official Node.js 22 and later Linux builds, so any glibc system that runs official Node 22 can load the addon. They are built in the manylinux_2_28 images (glibc 2.28), and the build fails if an addon needs a newer glibc symbol version.

Distribution glibc
RHEL, AlmaLinux, Rocky Linux 8 2.28
Debian 11 2.31
Ubuntu 20.04 2.31
RHEL, AlmaLinux, Rocky Linux 9 2.34
Amazon Linux 2023, AWS Lambda nodejs22.x and nodejs24.x 2.34
Ubuntu 22.04 2.35
Debian 12 2.36
Ubuntu 24.04 2.39

Any later release of these works too. Amazon Linux 2 (glibc 2.26) is not supported: it reached its end of life on 2026-06-30, and official Node 22 doesn’t run on it either. Alpine and other musl systems use the linux-x64-musl and linux-arm64-musl addons instead.

The package loads only the polyjose addon whose SHA-256 it was released with, from its own folder. Otherwise the import throws, and nothing it refused has run. It doesn’t use a generated napi-rs loader, so it never searches for the addon.

  • The platform. The package works out your platform name, for example linux-arm64-musl (see Side effects of loading for how it tells musl from glibc). A platform outside the eight above is refused.
  • The hash pin. The package ships addon-sha256.json: the SHA-256 of each of its eight addons, generated when the package is built. The package reads polyjose.<platform>.node from its own folder, by its full path, hashes it, and refuses it unless the hash is the pinned one. The refusal names the platform, the file and both hashes. There is no fallback to another file, and no other place is ever looked at: not node_modules folders (the package’s own or a parent’s), not NODE_PATH, not an environment variable. So a substitute planted anywhere is never loaded, and one put in place of the addon is refused by its hash, a native addon as much as a JavaScript file. A package without the map, or without an entry for your platform, loads nothing.
  • No swap after the check. The package writes the bytes it checked to a new private directory in os.tmpdir() (mode 0700; on Windows, your own %TEMP%), checks the copy again, and loads exactly that copy. Then it deletes the copy; the loaded addon stays in memory. Windows can’t delete a loaded DLL, so there each copy (the size of the addon) stays in the temporary directory. The temporary directory must allow loading native code: if yours is mounted noexec, point TMPDIR (TEMP on Windows) at one that does.
  • After loading, the result must be exactly the checked copy, not wrapped by any JavaScript, and its own version() must be this package’s version. An addon from another polyjose release is refused.
  • No WebAssembly fallback. polyjose never publishes a wasm32-wasi package, and the package has no WASI fallback: there is no generated loader to try one. If the operating system can’t load the checked copy, the import throws Node’s own error for loading a native addon. For WebAssembly, use @polyjose/wasm.

These checks keep the package from choosing a substitute. They can’t protect against code that already runs in your process, which can change anything.

Every refusal above throws a plain Error, not a PolyjoseError, with code set to ERR_POLYJOSE_ADDON_REFUSED. It happens at import, before any operation runs. Its message names your platform, and for a hash mismatch also the file and both SHA-256 values. There is one code for every kind of refusal, because none of them is something your code can recover from: reinstall the package, or, on an unsupported platform, use @polyjose/wasm.

The package reads no environment variable to decide what to load. The napi-rs variables below change other napi-rs packages, but not this one; they are listed because 0.1.1 and earlier read or refused them.

Variable Effect
NAPI_RS_NATIVE_LIBRARY_PATH Ignored. The addon always comes from the package’s own folder. (Up to 0.1.1, a file at this path was loaded if its SHA-256 was the pinned one.)
NAPI_RS_FORCE_WASI Ignored. There is no WASI fallback. (Up to 0.1.1, the import threw if it was set.)
NAPI_RS_WASI_FLAVOR Ignored, like NAPI_RS_FORCE_WASI.
NAPI_RS_ENFORCE_VERSION_CHECK Ignored. The package always checks the addon’s hash and version.

The temporary directory comes from os.tmpdir(), so TMPDIR (TEMP on Windows) decides where the private copy goes (see “No swap after the check” above).

If the Rust code ever panics, Node.js aborts the whole process (SIGABRT). The Promise isn’t rejected, and no catch or process.on("uncaughtException") handler runs. This fails closed: no partial result is returned, and no memory is corrupted. Today we know of no input that causes a panic; the core forbids panic!, unwrap and expect. A server that must survive a panic should run polyjose in a worker process it can restart. Turning panics into rejected Promises is planned.

  • Linux: to tell musl from glibc, the package reads /usr/bin/ldd, as napi-rs does. If it can’t be read, the package calls process.report.getReport(), with process.report.excludeNetwork set to true while the report is taken and restored to your value after. If there is no report either, the import throws instead of guessing. No child process is run.
  • Windows: the package loads the msvc addon for your architecture, also when Node runs embedded as a shared library.
  • Temporary directory: each import that loads the addon creates one directory in os.tmpdir() (see “No swap after the check” above).

Results are plain objects, and their byte fields are plain Uint8Arrays. So printing a result prints its secret bytes: console.log, util.inspect, JSON.stringify and error reporters show every byte. The Kotlin and Swift bindings redact these fields when printed; JavaScript keeps plain data instead.

  • Never log a JwsVerified or JweDecrypted result.
  • Never log the options and records that carry a secret: JwsVerifyOptions (detachedPayload), JweEncryptOptions (aad), JwsSigner and JweRecipient (key). Never log a private JWK.
  • The seven secret fields are JwsSigner.key, JweRecipient.key, JwsVerified.payload, JweDecrypted.plaintext, JweDecrypted.aad, JweEncryptOptions.aad and JwsVerifyOptions.detachedPayload.
  • When you are done with a returned Uint8Array, wipe it with fill(0). JavaScript strings, such as a JWK’s text, can’t be wiped.

Secrets in memory covers what every platform does with secrets.

The package is MIT licensed. Its one THIRD-PARTY-NOTICES.txt lists the third-party code it contains: the Rust crates compiled into its eight addons, for every target. Its JavaScript is polyjose’s own.