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

prisma-shield

v0.1.1

Published

Prisma Client Extension for field encryption, row-level security, audit logging, and data masking

Readme


One config. Zero changes to your business logic. Drop-in $extends and your data is encrypted, isolated, audited, and masked.

const prisma = new PrismaClient().$extends(shield({ ... }))

Features

| Module | What it does | |--------|-------------| | Encryption | AES-256-GCM per-field encryption with searchable blind indexes (HMAC-SHA256) | | Row-Level Security | Automatic tenant/user isolation — where-clause injection on every query | | Audit Logging | Fire-and-forget operation logging with pluggable adapters | | Data Masking | Role-based field masking: admin sees all, support sees partial, analyst sees ******** | | Key Rotation | Re-encrypt all data when rotating keys |

Each module is optional. Use any combination.


Install

npm install prisma-shield

Quick Start

1. Generate keys:

npx prisma-shield generate-keys
# ENCRYPTION_KEY=...
# BLIND_INDEX_KEY=...

2. Add _idx columns for each encrypted field:

model User {
  id        Int     @id @default(autoincrement())
  email     String  // will store ciphertext
  email_idx String? // blind index for search
  ssn       String
  ssn_idx   String?
  name      String
}

3. Wrap your client:

import { PrismaClient } from '@prisma/client'
import { shield } from 'prisma-shield'
import { consoleAdapter } from 'prisma-shield/adapters'

const prisma = new PrismaClient().$extends(
  shield({
    encrypt: {
      key: process.env.ENCRYPTION_KEY!,
      blindIndexKey: process.env.BLIND_INDEX_KEY!,
      fields: { user: ['email', 'ssn'] },
    },
    rls: {
      context: () => getRequestContext(),
      policies: { user: (ctx) => ({ tenantId: ctx.tenantId }) },
      bypassRoles: ['superadmin'],
    },
    audit: {
      enabled: true,
      adapter: consoleAdapter(),
    },
    mask: {
      rules: {
        user: {
          email: { admin: 'none', support: 'partial', analyst: 'full' },
          ssn:   { admin: 'partial', support: 'full', analyst: 'full' },
        },
      },
    },
  })
)

4. Use Prisma as usual — security is transparent:

// Data is encrypted in DB, blind index enables search
await prisma.user.create({
  data: { email: '[email protected]', ssn: '123-45-6789', name: 'John' },
})

// Search works transparently via blind index
const user = await prisma.user.findFirst({
  where: { email: '[email protected]' },
})

// Result is decrypted + masked based on role:
// admin:   { email: '[email protected]', ssn: '***-**-6789' }
// support: { email: 'j***@test.com', ssn: '********' }
// analyst: { email: '********',      ssn: '********' }

Modules

Encryption

AES-256-GCM encryption with random IV per write. HMAC-SHA256 blind indexes for searchable encrypted fields.

encrypt: {
  key: process.env.ENCRYPTION_KEY!,
  blindIndexKey: process.env.BLIND_INDEX_KEY!,
  fields: {
    user: ['email', 'ssn'],
    payment: ['cardNumber'],
  },
}

| Operation | What happens | |-----------|-------------| | Write | email → AES-256-GCM ciphertext, email_idx → HMAC-SHA256 | | Read | Ciphertext → decrypted plaintext, _idx fields removed | | Search | where: { email: 'x' }where: { email_idx: hmac('x') } |

Row-Level Security

Every query gets a where-clause injected from your policy. Creates auto-set policy fields.

rls: {
  context: () => ({
    userId: getCurrentUserId(),
    tenantId: getCurrentTenantId(),
    role: getCurrentUserRole(),
  }),
  policies: {
    user: (ctx) => ({ tenantId: ctx.tenantId }),
    post: (ctx) => ({ authorId: ctx.userId }),
  },
  bypassRoles: ['superadmin'],
}
  • findMany, update, delete → AND-merge policy into where
  • create, createMany → auto-set policy fields from context
  • bypassRoles → skip RLS entirely for these roles

Audit Logging

Fire-and-forget — never blocks your query, never crashes your app.

import { consoleAdapter } from 'prisma-shield/adapters'

audit: {
  enabled: true,
  adapter: consoleAdapter(),
  logReads: false,      // default
  sanitize: true,       // encrypted values → [ENCRYPTED]
  include: ['user'],    // optional: only these models
  exclude: ['session'], // optional: skip these models
}

Adapters: consoleAdapter() | prismaAdapter(client) | custom { log(entry) {} }

Data Masking

Role-based masking applied after decryption. Unknown roles get 'full' mask (secure by default).

mask: {
  rules: {
    user: {
      email: { admin: 'none', support: 'partial', analyst: 'full' },
      phone: { admin: 'none', support: 'partial' },
    },
  },
}

| Strategy | Output | |----------|--------| | 'none' | [email protected] | | 'full' | ******** | | 'partial' | j***@test.com / ****4567 / ***-**-6789 | | 'hash' | a1b2c3d4 (consistent SHA256 prefix) | | (v) => string | Custom function |


Key Rotation

import { rotateKeys } from 'prisma-shield'

const users = await rawPrisma.user.findMany()

const result = rotateKeys(users, ['email', 'ssn'], {
  oldKey: process.env.OLD_ENCRYPTION_KEY!,
  newKey: process.env.NEW_ENCRYPTION_KEY!,
  oldBlindIndexKey: process.env.OLD_BLIND_INDEX_KEY!,
  newBlindIndexKey: process.env.NEW_BLIND_INDEX_KEY!,
})

for (const { id, data } of result.updates) {
  await rawPrisma.user.update({ where: { id }, data })
}

console.log(`Rotated: ${result.processed}, Failed: ${result.failed}`)

Performance

Benchmarks (10,000 iterations, Node.js):

| Operation | Time | |-----------|------| | AES encrypt | 0.011ms | | AES decrypt | 0.006ms | | Blind index (HMAC) | 0.004ms | | Full round-trip (2 fields) | 0.044ms |

Target: < 5ms per operation. Actual: 0.044ms (113x under budget).


Limitations

  • Blind index: exact match only (no LIKE, contains, range queries)
  • RLS: does not apply to nested include relations
  • Encryption: String fields only
  • $queryRaw / $executeRaw bypass the pipeline
  • Masking requires RLS context (rls.context must be configured)

License

MIT