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.
Platform support
Section titled “Platform support”@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.
Linux glibc floor
Section titled “Linux glibc floor”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.
How the addon is loaded
Section titled “How the addon is loaded”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 readspolyjose.<platform>.nodefrom 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: notnode_modulesfolders (the package’s own or a parent’s), notNODE_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()(mode0700; 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 mountednoexec, pointTMPDIR(TEMPon 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-wasipackage, 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.
ERR_POLYJOSE_ADDON_REFUSED
Section titled “ERR_POLYJOSE_ADDON_REFUSED”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.
Environment variables
Section titled “Environment variables”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).
A panic in the addon aborts the process
Section titled “A panic in the addon aborts the process”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.
Side effects of loading
Section titled “Side effects of loading”- Linux: to tell musl from glibc, the package reads
/usr/bin/ldd, as napi-rs does. If it can’t be read, the package callsprocess.report.getReport(), withprocess.report.excludeNetworkset totruewhile 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
msvcaddon 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).
Secrets in results
Section titled “Secrets in results”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
JwsVerifiedorJweDecryptedresult. - Never log the options and records that carry a secret:
JwsVerifyOptions(detachedPayload),JweEncryptOptions(aad),JwsSignerandJweRecipient(key). Never log a private JWK. - The seven secret fields are
JwsSigner.key,JweRecipient.key,JwsVerified.payload,JweDecrypted.plaintext,JweDecrypted.aad,JweEncryptOptions.aadandJwsVerifyOptions.detachedPayload. - When you are done with a returned
Uint8Array, wipe it withfill(0). JavaScript strings, such as a JWK’s text, can’t be wiped.
Secrets in memory covers what every platform does with secrets.
Licenses
Section titled “Licenses”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.