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

zenstack-encryption

v0.2.0

Published

ZenStack Encryption Plugin - Automatic field encryption/decryption for @encrypted fields

Readme

zenstack-encryption

A ZenStack v3 community plugin that provides transparent field-level encryption and decryption using the @encrypted attribute.

Features

  • AES-256-GCM encryption via the Web Crypto API (no native dependencies)
  • Transparent encrypt-on-write, decrypt-on-read through ZenStack's onQuery plugin hook
  • Key rotation — add previous keys to a fallback list so existing data can still be decrypted while new writes use the latest key
  • Custom encryption — bring your own encrypt/decrypt functions for KMS integration, envelope encryption, etc.
  • Nested writes — handles create, createMany, update, updateMany, upsert, and connectOrCreate across relations

How It Works

The plugin hooks into the ZenStack ORM's query lifecycle via onQuery. When a write operation (create, update, etc.) is performed, the plugin inspects the schema for fields marked @encrypted and encrypts their values before they reach the database. When data is read back, encrypted fields are automatically decrypted before being returned to the caller.

Write path:  app → plugin encrypts @encrypted fields → database
Read path:   database → plugin decrypts @encrypted fields → app

Encrypted values are stored as a base64 string with the format {metadata}.{ciphertext}, where metadata includes the encryption version, algorithm, and a key digest (used for key rotation lookups). Each encryption uses a random 12-byte IV, so the same plaintext produces different ciphertext every time.

Note: Because the plugin operates at the ORM level, direct Kysely query builder calls (client.$qb) bypass encryption entirely.

Installation

# npm
npm install zenstack-encryption

# pnpm
pnpm add zenstack-encryption

Setup

1. Register the plugin in your ZModel schema

Add a plugin block to your .zmodel file. This makes the @encrypted attribute available in your schema:

plugin encryption {
    provider = 'zenstack-encryption'
}

2. Mark fields with @encrypted

Apply @encrypted to any String field you want to encrypt at rest:

model User {
    id          String @id @default(cuid())
    email       String @unique
    name        String?
    secretToken String @encrypted
    posts       Post[]
}

model Post {
    id        String @id @default(cuid())
    title     String
    content   String? @encrypted
    author    User   @relation(fields: [authorId], references: [id])
    authorId  String
}

3. Generate your schema

npx zenstack generate

4. Configure the plugin at runtime

import { ZenStackClient } from '@zenstackhq/orm';
import { encryption } from 'zenstack-encryption';
import schema from './schema.js';

// Pass a string secret — it will be derived to a 32-byte key via SHA-256
const client = new ZenStackClient(schema, {
    plugins: [
        encryption({
            key: process.env.ENCRYPTION_SECRET!,
        }),
    ],
});

// Or pass a raw 32-byte Uint8Array if you already have one
// encryption({
//     key: new Uint8Array(Buffer.from(process.env.ENCRYPTION_KEY!, 'base64')),
// })

// Fields are encrypted/decrypted transparently
const user = await client.user.create({
    data: {
        email: '[email protected]',
        secretToken: 'super-secret-value',
    },
});

console.log(user.secretToken); // → "super-secret-value" (decrypted)

Key Rotation

When you need to rotate encryption keys, pass old keys via previousKeys. The plugin will use the primary key for new writes, but try all keys (key + previousKeys) when decrypting. Both strings and Uint8Array keys can be mixed:

const plugin = encryption({
    key: 'new-secret',          // used for all new encryptions
    previousKeys: ['old-secret'],       // tried during decryption alongside key
});

This enables zero-downtime key rotation:

  1. Deploy with both keys configured (new key as primary, old key in previousKeys)
  2. Existing data encrypted with the old key is still readable
  3. New writes use the new key
  4. Optionally re-encrypt old data by reading and updating records

Custom Encryption

For integration with AWS KMS, HashiCorp Vault, or any other encryption provider, pass custom encrypt and decrypt functions:

import type { FieldDef } from '@zenstackhq/orm/schema';

const plugin = encryption({
    encrypt: async (model: string, field: FieldDef, plaintext: string) => {
        // Call your encryption service
        return await myKms.encrypt(plaintext);
    },
    decrypt: async (model: string, field: FieldDef, ciphertext: string) => {
        // Call your decryption service
        return await myKms.decrypt(ciphertext);
    },
});

The model and field parameters let you use different keys or strategies per model/field.

Adding to an existing client

You can also add the plugin to an existing ZenStackClient instance using $use:

const baseClient = new ZenStackClient(schema);
const client = baseClient.$use(encryption({ key }));

Security Notes

When passing a string as key, the plugin derives a 32-byte key using SHA-256. This is not a password-based key derivation function — it does not use salting or iterations. Your string secret should be high-entropy (e.g. a random 32+ character token from a secrets manager, not a human-chosen password).

# Good: generate a random secret
openssl rand -base64 32

# Bad: weak password
ENCRYPTION_SECRET="password123"

If you need to derive keys from low-entropy passwords, use a proper KDF (PBKDF2, Argon2) yourself and pass the resulting Uint8Array directly.

Limitations

  • ORM only — only applies to ORM CRUD operations, not direct Kysely query builder calls via client.$qb
  • String fields only@encrypted can only be applied to String fields. Applying @encrypted to non-String fields will log a warning at runtime and be ignored.
  • No encrypted filtering — encrypted fields cannot be used in where clauses, orderBy, or unique constraints. Since encryption is non-deterministic (each encryption produces different ciphertext due to random IVs), queries like where: { secretField: 'value' } will never match. If you need to search by a field, don't encrypt it — or store a separate non-encrypted hash for lookups.
  • Storage overhead — encrypted values are larger than the original plaintext. Expect roughly 80 bytes of overhead per field (IV + GCM tag + metadata + base64 encoding), plus ~37% expansion of the plaintext itself. A 100-character plaintext becomes ~215 characters. Ensure your database columns use TEXT or a sufficiently large VARCHAR.

License

MIT