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

@vonpay/crypto

v0.1.1

Published

AES-256-GCM encrypt/decrypt with key versioning + retired-key fallback, plus HKDF-SHA-256 per-merchant key derivation

Readme

@vonpay/crypto

Cryptographic primitives used by Vonpay services to encrypt secrets at rest. AES-256-GCM encrypt / decrypt with retired-key fallback, plus HKDF-SHA-256 per-merchant key derivation under a versioned master-key registry.

Internal — the surface is designed for Vonpay use cases (per-merchant credential encryption for downstream services) but the building blocks are standard RFC primitives with no Vonpay-specific business logic.

Why

Vonpay services need to encrypt PII and provider credentials at rest. This package centralises the primitive so every service uses the same AES-256-GCM construction and the same key-rotation discipline. It is intentionally env-agnostic at the crypto coreencrypt / decrypt never read process.env; callers pass parsed keys in. The per-merchant-key subpath does ship a small env-reading helper (loadRegistryFromEnv) for the per-merchant derivation flow, scoped to a single well-known env-var prefix that callers can override.

Install

npm install @vonpay/crypto

Inside the vonpay monorepo, depend on it as a workspace package:

{
  "dependencies": {
    "@vonpay/crypto": "workspace:*"
  }
}

Requires Node 20+. ESM only.

Root API — AES-256-GCM encrypt/decrypt

import { encrypt, decrypt, parseHexKey } from "@vonpay/crypto";

// Caller owns env loading.
const activeKey = parseHexKey(process.env.MY_ENCRYPTION_KEY!);
const retiredKeys = (process.env.MY_ENCRYPTION_KEYS_RETIRED ?? "")
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean)
  .map(parseHexKey);

const encoded = encrypt("[email protected]", activeKey);

// Try active first, then retired keys (allows in-place key rotation).
const plaintext = decrypt(encoded, [activeKey, ...retiredKeys]);

Wire format

base64( iv (12 bytes) || tag (16 bytes) || ciphertext ). Always GCM, always 12-byte IV, always 16-byte tag.

Direct-key rotation (single-key consumers)

For services with a single master key and no per-merchant derivation (the original 0.1.0 model):

  1. Generate a new key: openssl rand -hex 32
  2. Move the current key value into your MY_ENCRYPTION_KEYS_RETIRED env (comma-separated).
  3. Set MY_ENCRYPTION_KEY to the new key.
  4. Restart the service. New writes use the new key; old rows continue to decrypt via the retired key.
  5. Bump your service's key-version tag (e.g. v1v2) on the same deploy so rows tag the new key. Retired keys can be dropped after every row has been re-encrypted via a backfill.

Subpath API — per-merchant key derivation

import { encrypt, decrypt } from "@vonpay/crypto";
import {
  deriveMerchantKey,
  loadRegistryFromEnv,
} from "@vonpay/crypto/per-merchant-key";

// Load master keys once at boot. Default prefix VONPAY_MASTER_KEY_V*;
// pass a custom prefix to namespace per product.
const registry = loadRegistryFromEnv();

// At write time: derive under the current version, persist the version next to the ciphertext.
const merchantId = "1f1c8c34-7a09-4b7a-9a3a-1d2e3f405060";
const { key, version } = deriveMerchantKey({
  registry,
  merchantId,
  purpose: "access-token",
});
const ciphertext = encrypt("shpat_…", key);
// INSERT ... (encrypted_access_token, encryption_key_version) VALUES (ciphertext, version)

// At read time: derive under the stored version.
const { key: readKey } = deriveMerchantKey({
  registry,
  merchantId,
  purpose: "access-token",
  version: storedRow.encryption_key_version,
});
const plaintext = decrypt(storedRow.encrypted_access_token, [readKey]);

HKDF parameter shape

Identical for every consumer:

  • IKM — the master key for the requested version (VONPAY_MASTER_KEY_V<N> by default; consumers can pass a custom prefix to loadRegistryFromEnv)
  • saltSHA-256(merchantId) (deterministic per merchant, non-secret)
  • infovonpay-merchant-${purpose}-v${version} (the embedded version guarantees v1 and v2 keys are cryptographically distinct even for the same (merchantId, purpose))
  • L — 32 bytes (AES-256 key length)

purpose is a free-form string validated against /^[a-z][a-z0-9-]{0,31}$/ — lowercase, starts with a letter, hyphen-separated, ≤32 chars. Currently defined purposes are "access-token" and "webhook-secret"; downstream consumers add their own without a breaking change.

Canonical purposes — to avoid cryptographic-distinct-key collisions across call sites that meant to derive the same key, treat the purpose string as a coordinated identifier. Misspelling "webhook-secret" as "wh-secret" derives a different (irrecoverable) key. Confine purpose strings to a small, reviewed set per consumer.

N-version master-key rotation

The registry holds every version that is currently loaded — not just two. To rotate:

  1. Generate a new key: openssl rand -hex 32
  2. Set the new key as VONPAY_MASTER_KEY_V<N+1> (where <N> is the current highest version, using the default prefix; substitute a custom prefix where you set one). Keep _V<N> set.
  3. Restart the service. The registry now contains both versions; currentVersion automatically becomes N+1. New writes derive under N+1; existing rows continue to decrypt via their stored version.
  4. (At leisure) Run a backfill that reads each row, decrypts under its stored version, re-encrypts under the current key, and updates encryption_key_version. Stop the backfill worker before counting — an in-flight batch can leave rows referencing the old version even after the count query returns zero. Verify zero on two consecutive reads separated by at least one retry cycle.
  5. After the backfill confirms zero rows reference <N>, unset VONPAY_MASTER_KEY_V<N> and restart. The retired version is no longer loaded.

No _NEXT flag, no "rotation window" mode — adding a higher-numbered env var IS the rotation signal.

License

MIT