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

@rnpgp/rnp

v0.1.1

Published

WASM build of rnp (RFC 9580 OpenPGP) with idiomatic TypeScript bindings.

Readme

@rnpgp/rnp

CI Tests License npm

npm: @rnpgp/rnp · GitHub: rnpgp/rnp-wasm

WASM build of rnp (RFC 9580 OpenPGP C++ library) with idiomatic TypeScript bindings. Built on Emscripten 3.1.74

  • Botan 3.12.0, runnable in Node.js (≥18.18) and modern browsers (WASM + WebCrypto-agnostic; uses Embind + FinalizationRegistry).

Note on naming: the npm package is @rnpgp/rnp (matches the sibling bindings ruby-rnp, php-rnp, swift-rnp). The GitHub repo is rnp-wasm for historical reasons; both refer to the same project.

Status: alpha. Core API surface (Ffi, Keyring, Key, sign/verify, encrypt/decrypt, streaming, packet dump, registries) is stable and 100% covered by tests. The npm package is not yet published — install from source until v0.1.0.

What it does

  • Generate, import, export, inspect OpenPGP keys (RSA, ECDSA, EDDSA, X25519)
  • Sign and verify messages (binary, cleartext, detached)
  • Encrypt and decrypt messages (public-key or password-based)
  • Stream bytes in/out via sync callbacks (Asyncify pending)
  • Enumerate signatures, subkeys, UIDs
  • Convert armored ↔ binary
  • Inspect packet structure via JSON dump
  • Algorithm registry with OCP — extend without changing renderer code

Install

npm install @rnpgp/rnp

Requirements

  • Node.js ≥ 18.18 (uses FinalizationRegistry, WeakRef, top-level await)
  • Or any modern browser (Chromium 84+, Firefox 79+, Safari 14.1+)
  • The .wasm file is ~10 MB (~3 MB gzipped) — budget accordingly

Quick start

import { initRnp } from "@rnpgp/rnp";

const rnp = await initRnp();
using ffi = rnp.createFfi();

// Load a public key (armored ASCII)
const armoredKey = new Uint8Array(fs.readFileSync("alice.asc"));
using input = ffi.input(armoredKey);
ffi.keyring.load("GPG", input);

// Locate the key by userid
using alice = ffi.keyring.mustLocate("userid", "[email protected]");
console.log("Alice's fingerprint:", alice.fingerprint);

// ffi is auto-disposed via `using` (TC39 explicit-resource-management)

Sign + verify

import { initRnp, SignOperation, VerifyOperation } from "@rnpgp/rnp";

const rnp = await initRnp();
using ffi = rnp.createFfi();
ffi.setPasswordProvider(() => "testkey");

// Load the signer's secret key
using sk = ffi.input(secretKeyBytes);
using pk = ffi.input(publicKeyBytes);
ffi.keyring.load("GPG", sk);
ffi.keyring.load("GPG", pk);
using signer = ffi.keyring.mustLocate("userid", "[email protected]");
signer.unlock("testkey");

// Sign
const message = new TextEncoder().encode("hello rnp-wasm");
using input = ffi.input(message);
using output = ffi.output();
using op = SignOperation
  .create(ffi, input, output, "binary")
  .addSignature(signer, { hash: "SHA256" });
op.execute();
const signed = output.bytes();

// Verify
using vIn = ffi.input(signed);
using vOut = ffi.output();
using vOp = VerifyOperation.create(ffi, vIn, vOut);
const result = vOp.execute();
if (result.signatures[0]?.valid) {
  console.log("✓ signature valid");
}

Three sign modes: "binary" (default), "cleartext" (ASCII-armored signed message), "detached" (signature separate from message).

Encrypt + decrypt

import { EncryptOperation, decrypt } from "@rnpgp/rnp";

// Public-key encryption
const plaintext = new TextEncoder().encode("secret");
using in1 = ffi.input(plaintext);
using out1 = ffi.output();
using eop = EncryptOperation.create(ffi, in1, out1).addRecipient(recipient);
eop.execute();
const ciphertext = out1.bytes();

// Decrypt (sync password provider; Asyncify needed for async providers)
using in2 = ffi.input(ciphertext);
using out2 = ffi.output();
decrypt(ffi, in2, out2);
const recovered = out2.bytes();
expect(recovered).toEqual(plaintext);

Password-based encryption:

using op = EncryptOperation
  .create(ffi, in1, out1)
  .addPassword("hunter2", { hash: "SHA256" });

Key generation

import { GenerateOperation } from "@rnpgp/rnp";

// RSA primary key (sign + certify + encrypt)
using op = GenerateOperation
  .rsa(ffi, 2048, "alice <[email protected]>")
  .addUsage("sign")
  .addUsage("certify")
  .addUsage("encrypt")
  .protection("passphrase");
const alice = op.execute();
console.log(alice.fingerprint);

// ECDSA on NIST P-256
using ecdsaOp = GenerateOperation
  .ecdsa(ffi, "NIST P-256", "bob <[email protected]>");

// Ed25519 (signing-only)
using eddsaOp = GenerateOperation.eddsa(ffi, "carol <[email protected]>");

// X25519 subkey (encrypt-only — must be a subkey under a signing primary)
using subOp = GenerateOperation.createSubkey(ffi, alice, "X25519");
const x25519 = subOp.execute();

Streaming

Sync callbacks; ideal for piping file/socket data through rnp without buffering the whole message in memory:

import { StreamInput, StreamOutput, SignOperation } from "@rnpgp/rnp";

let offset = 0;
using sIn = StreamInput.create(ffi, (buf) => {
  const remaining = source.length - offset;
  if (remaining === 0) return null;  // EOF
  const n = Math.min(buf.length, remaining);
  buf.set(source.subarray(offset, offset + n));
  offset += n;
  return n;
});

const chunks: Uint8Array[] = [];
using sOut = StreamOutput.create(ffi, (chunk) => {
  chunks.push(chunk.slice());  // copy — chunk is a transient view
  return true;
});

using op = SignOperation.create(ffi, sIn, sOut, "binary").addSignature(signer);
op.execute();
sOut.finish();

Async streaming (WHATWG ReadableStream / WritableStream) requires the Asyncify build variant — see RNPWASM_ASYNCIFY=1 in cmake/Emscripten.cmake.

API surface

Entry point: initRnp(opts?: InitOptions): Promise<Rnp>. From there:

  • Rnp.createFfi()Ffi — top-level per-session handle
  • Ffi.input(bytes) / Ffi.output() — memory-backed I/O
  • Ffi.keyringKeyring (load, save, import, export, locate)
  • Ffi.dumpPacketDump (packet inspection)
  • Ffi.setPasswordProvider(...) — callback for secret-key unlock prompts
  • Keyring.mustLocate(type, id) — returns Key or throws
  • Key.fingerprint, .keyid, .algorithm, .bits, .curve, .userIds(), .subkeys()
  • Key.export(out, { armored, secret, includeSubkeys }) — armored or binary
  • Key.unlock(passphrase) / Key.lock() — temporary unlock
  • Key.packetsToJson(flags) — packet-level inspection
  • SignOperation.create(...).addSignature(...).execute()
  • VerifyOperation.create(...).execute()VerifyResult
  • EncryptOperation.create(...).addRecipient(...).execute()
  • decrypt(ffi, in, out) — one-shot (rnp 0.18.1 has no op-decrypt API)
  • GenerateOperation.rsa(...) / .ecdsa(...) / .eddsa(...) / .createSubkey(...)

Operations follow TC39 explicit-resource-management — wrap in using for automatic cleanup:

using ffi = rnp.createFfi();
using op = SignOperation.create(ffi, input, output);
op.execute();
// ffi.destroy() + op.destroy() called automatically at end of scope

Algorithm registries

Algorithm names match rnp's RNP_ALGNAME_* defines (rnp/rnp.h). Use the registries for type-safe lookups:

import {
  HashAlgorithms, SymmetricAlgorithms, PublicKeyAlgorithms,
  AeadAlgorithms, CompressionAlgorithms, Curves,
} from "@rnpgp/rnp";

HashAlgorithms.lookup("sha256");       // → "SHA256"
HashAlgorithms.lookup("SHA-256");      // → "SHA256" (alias)
HashAlgorithms.lookup("SHA256");       // → "SHA256"
SymmetricAlgorithms.lookup("AES-256"); // → "AES256"
Curves.lookup("secp256r1");            // → "NIST P-256"

Build-time feature snapshot at dist/features.json (generated from rnp_supported_features). Use rnp.features() to inspect at runtime.

Build from source

Prerequisites

  • Emscripten 3.1.74+ (Docker) or Homebrew emscripten for native macOS dev
  • Docker (for reproducible builds)
  • Node.js ≥ 18.18 (for tests + features snapshot)

Native build (AArch64 macOS, fastest iteration)

brew install emscripten
git clone --recursive https://github.com/rnpgp/rnp-wasm.git
cd rnp-wasm
npm install
npm run build           # → dist/rnp.{wasm,js}, features.json
npm test

Set DEPS_PREFIX / RNP_BUILD / BINDINGS_BUILD env vars if you want to keep the build dir outside /opt (the script defaults):

DEPS_PREFIX=$PWD/build/deps \
RNP_BUILD=$PWD/build/rnp \
BINDINGS_BUILD=$PWD/build/bindings \
npm run build

Reproducible Docker build

docker build -t rnp-wasm:local .
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" rnp-wasm:local \
  scripts/build.sh

Build pipeline

scripts/build.sh orchestrates three stages:

  1. scripts/build-deps.sh — Botan 3.12.0 + json-c 0.17 from source via emconfigure; zlib + bzip2 via Emscripten ports
  2. scripts/build-rnp.sh — rnp 0.18.1 (git submodule) with the 01-botan-3.7-ec_group-include.patch for Botan 3.7+ compatibility
  3. scripts/build-bindings.sh — Embind C++ bindings + final link to dist/rnp.{wasm,js} + dist/features.json

Architecture notes (load-bearing details)

  • Native Wasm EH (-fwasm-exceptions) is required consistently across Botan, rnp, and the bindings. Botan 3.11+ selects this automatically; pre-3.11 used SjLj (-sDISABLE_EXCEPTION_CATCHING=0). Mixing modes produces either link errors or runtime traps.
  • Stack size ≥ 2 MB (-sSTACK_SIZE=2MB). The default 64 KB overflows deep in Botan's PK_Signer dispatch; the resulting "null function or function signature mismatch" trap is misleading — it's actually stack corruption. See cmake/Emscripten.cmake for the centralized flags.
  • ENABLE_CRYPTO_REFRESH=ON in scripts/build-rnp.sh enables PGP_PKA_X25519 and PGP_PKA_ED25519 (the Crypto-Refresh variants, as opposed to the legacy EDDSA alias).

Testing

npm test                 # vitest, Node + WASM
npm run test:browser     # Playwright, chromium/firefox/webkit
npm run typecheck        # tsc --noEmit
npm run lint             # ESLint 9 flat config

82 tests covering: lifecycle, registry, bytes, keyring, key, sign-verify (4 modes), encrypt-decrypt (public-key + password-based), generate (RSA/ECDSA/EDDSA/X25519-subkey), dump, signature inspection, password-provider, key-enumeration, armor, iterator, key-equality, key-export, streaming, negative paths.

Project structure

rnp-wasm/
├── src/cpp/             Embind C++ bindings (18 modules)
│   ├── bindings/        One .cpp per rnp feature area
│   └── handle.h         RAII template for rnp opaque handles
├── ts/                  TypeScript wrapper (operations, registries, providers)
├── third-party/
│   ├── rnp/             rnp 0.18.1 (git submodule)
│   └── patches/         Source patches for rnp
├── scripts/             build.sh, build-deps.sh, build-rnp.sh, build-bindings.sh
├── cmake/               Emscripten.cmake, ImportedLib.cmake
├── test/node/           vitest test suite
├── test/browser/        Playwright smoke tests + harness
├── docs/                API reference (typedoc), worked examples
├── Dockerfile           Reproducible build image
└── dist/                Generated: module.{wasm,js}, index.{js,d.ts}, features.json

Optional features

  • Worker pool (WorkerPool): offload crypto to Web Workers via Comlink. Re-exported from the main entry — see docs/examples/worker-pool.ts for a runnable example. Requires a bundler that understands the new Worker(new URL("./worker.js", import.meta.url)) pattern (Vite, webpack 5+).
  • Async (JSPI) variant: dist/module-async.{js,wasm} is built with scripts/build.sh --async. Supports async password providers and streaming. Excluded from the npm tarball by default (~10 MB); install from source or wait for a dedicated @rnpgp/rnp-async package.
  • PQC variant: Botan build with --variant pqc adds ML-KEM, ML-DSA, SLH-DSA modules. See variants.json.

Release flow

Releases publish to npm from .github/workflows/release.yml. Two ways to cut a release:

  1. Tag pushgit tag v0.1.2 && git push origin v0.1.2
  2. Actions UI — https://github.com/rnpgp/rnp-wasm/actions/workflows/release.yml → Run workflow → enter version. The workflow bumps package.json, publishes, and pushes the tag.

Both paths run lint + typecheck before publishing, then create a GitHub Release with auto-generated notes.

License

BSD-2-Clause. The compiled WASM bundles derive from rnp, Botan, zlib, and bzip2 — see LICENSE for full notices.

Contributing

See AGENTS.md for the seven architectural invariants and CONTRIBUTING.md for the contributor guide. All changes go through PRs — never commit directly to main.