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

@cipherstash/stack

v0.6.0

Published

CipherStash Stack for TypeScript and JavaScript

Downloads

546

Readme

@cipherstash/stack

The all-in-one TypeScript SDK for the CipherStash data security stack.

npm version License: MIT TypeScript

--

Table of Contents

--

Install

npm install @cipherstash/stack

Or with your preferred package manager:

yarn add @cipherstash/stack
pnpm add @cipherstash/stack

Quick Start

import { Encryption } from "@cipherstash/stack"
import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"

// 1. Define a schema
const users = encryptedTable("users", {
  email: encryptedColumn("email").equality().freeTextSearch(),
})

// 2. Create a client (reads CS_* env vars automatically)
const client = await Encryption({ schemas: [users] })

// 3. Encrypt a value
const encrypted = await client.encrypt("[email protected]", {
  column: users.email,
  table: users,
})

if (encrypted.failure) {
  console.error("Encryption failed:", encrypted.failure.message)
} else {
  console.log("Encrypted payload:", encrypted.data)
}

// 4. Decrypt the value
const decrypted = await client.decrypt(encrypted.data)

if (decrypted.failure) {
  console.error("Decryption failed:", decrypted.failure.message)
} else {
  console.log("Plaintext:", decrypted.data) // "[email protected]"
}

Features

  • Field-level encryption - Every value encrypted with its own unique key via ZeroKMS, backed by AWS KMS.
  • Searchable encryption - Exact match, free-text search, order/range queries, and encrypted JSONB queries in PostgreSQL.
  • Bulk operations - Encrypt or decrypt thousands of values in a single ZeroKMS call (bulkEncrypt, bulkDecrypt, bulkEncryptModels, bulkDecryptModels).
  • Identity-aware encryption - Tie encryption to a user's JWT via LockContext, so only that user can decrypt.
  • Secrets management - Store, retrieve, list, and delete encrypted secrets with the Secrets class.
  • CLI (stash) - Manage secrets from the terminal without writing code.
  • TypeScript-first - Strongly typed schemas, results, and model operations with full generics support.

Schema Definition

Define which tables and columns to encrypt using encryptedTable and encryptedColumn from @cipherstash/stack/schema.

import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema"

const users = encryptedTable("users", {
  email: encryptedColumn("email")
    .equality()         // exact-match queries
    .freeTextSearch()   // full-text search queries
    .orderAndRange(),   // sorting and range queries
})

const documents = encryptedTable("documents", {
  metadata: encryptedColumn("metadata")
    .searchableJson(),  // encrypted JSONB queries (JSONPath + containment)
})

Index Types

| Method | Purpose | Query Type | |----|-----|------| | .equality() | Exact match lookups | 'equality' | | .freeTextSearch() | Full-text / fuzzy search | 'freeTextSearch' | | .orderAndRange() | Sorting, comparison, range queries | 'orderAndRange' | | .searchableJson() | Encrypted JSONB path and containment queries | 'searchableJson' | | .dataType(cast) | Set the plaintext data type ('string', 'number', 'boolean', 'date', 'bigint', 'json') | N/A |

Methods are chainable - call as many as you need on a single column.

Encryption and Decryption

Single Values

// Encrypt
const encrypted = await client.encrypt("[email protected]", {
  column: users.email,
  table: users,
})

// Decrypt
const decrypted = await client.decrypt(encrypted.data)

Model Operations

Encrypt or decrypt an entire object. Only fields matching your schema are encrypted; other fields pass through unchanged.

The return type is schema-aware: fields matching the table schema are typed as Encrypted, while other fields retain their original types. For best results, let TypeScript infer the type parameters from the arguments:

type User = { id: string; email: string; createdAt: Date }

const user = {
  id: "user_123",
  email: "[email protected]",  // defined in schema -> encrypted
  createdAt: new Date(),       // not in schema -> unchanged
}

// Let TypeScript infer the return type from the schema
const encryptedResult = await client.encryptModel(user, users)
// encryptedResult.data.email    -> Encrypted
// encryptedResult.data.id       -> string
// encryptedResult.data.createdAt -> Date

// Decrypt a model
const decryptedResult = await client.decryptModel(encryptedResult.data)

Bulk Operations

All bulk methods make a single call to ZeroKMS regardless of the number of records, while still using a unique key per value.

Bulk Encrypt / Decrypt (raw values)

const plaintexts = [
  { id: "u1", plaintext: "[email protected]" },
  { id: "u2", plaintext: "[email protected]" },
  { id: "u3", plaintext: "[email protected]" },
]

const encrypted = await client.bulkEncrypt(plaintexts, {
  column: users.email,
  table: users,
})

// encrypted.data = [{ id: "u1", data: EncryptedPayload }, ...]

const decrypted = await client.bulkDecrypt(encrypted.data)

// Each item has either { data: "plaintext" } or { error: "message" }
for (const item of decrypted.data) {
  if ("data" in item) {
    console.log(`${item.id}: ${item.data}`)
  } else {
    console.error(`${item.id} failed: ${item.error}`)
  }
}

Bulk Encrypt / Decrypt Models

const userModels = [
  { id: "1", email: "[email protected]" },
  { id: "2", email: "[email protected]" },
]

const encrypted = await client.bulkEncryptModels(userModels, users)
const decrypted = await client.bulkDecryptModels(encrypted.data)

Searchable Encryption

Encrypt a query term so you can search encrypted data in PostgreSQL.

// Equality query
const eqQuery = await client.encryptQuery("[email protected]", {
  column: users.email,
  table: users,
  queryType: "equality",
})

// Free-text search
const matchQuery = await client.encryptQuery("alice", {
  column: users.email,
  table: users,
  queryType: "freeTextSearch",
})

// Order and range
const rangeQuery = await client.encryptQuery("[email protected]", {
  column: users.email,
  table: users,
  queryType: "orderAndRange",
})

Searchable JSON

For columns using .searchableJson(), the query type is auto-inferred from the plaintext:

// String -> JSONPath selector query
const pathQuery = await client.encryptQuery("$.user.email", {
  column: documents.metadata,
  table: documents,
})

// Object/Array -> containment query
const containsQuery = await client.encryptQuery({ role: "admin" }, {
  column: documents.metadata,
  table: documents,
})

Batch Query Encryption

Encrypt multiple query terms in one call:

const terms = [
  { value: "[email protected]", column: users.email, table: users, queryType: "equality" as const },
  { value: "bob",               column: users.email, table: users, queryType: "freeTextSearch" as const },
]

const results = await client.encryptQuery(terms)

Query Result Formatting (returnType)

By default encryptQuery returns an Encrypted object (the raw EQL JSON payload). Use returnType to change the output format:

| returnType | Output | Use case | |---|---|---| | 'eql' (default) | Encrypted object | Parameterized queries, ORMs accepting JSON | | 'composite-literal' | string | Supabase .eq(), string-based APIs | | 'escaped-composite-literal' | string | Embedding inside another string or JSON value |

// Get a composite literal string for use with Supabase
const term = await client.encryptQuery("[email protected]", {
  column: users.email,
  table: users,
  queryType: "equality",
  returnType: "composite-literal",
})

// term.data is a string — use directly with .eq()
await supabase.from("users").select().eq("email", term.data)

Each term in a batch can have its own returnType.

PostgreSQL / Drizzle Integration Pattern

Encrypted data is stored as an EQL JSON payload. Install the EQL extension in PostgreSQL to enable searchable queries, then store encrypted data in eql_v2_encrypted columns.

The @cipherstash/stack/drizzle module provides encryptedType for defining encrypted columns and createEncryptionOperators for querying them:

import { pgTable, integer, timestamp } from "drizzle-orm/pg-core"
import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack/drizzle"
import { Encryption } from "@cipherstash/stack"

// Define schema with encrypted columns
const usersTable = pgTable("users", {
  id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
  email: encryptedType<string>("email", {
    equality: true,
    freeTextSearch: true,
    orderAndRange: true,
  }),
  profile: encryptedType<{ name: string; bio: string }>("profile", {
    dataType: "json",
    searchableJson: true,
  }),
})

// Initialize
const usersSchema = extractEncryptionSchema(usersTable)
const client = await Encryption({ schemas: [usersSchema] })
const ops = createEncryptionOperators(client)

// Query with auto-encrypting operators
const results = await db.select().from(usersTable)
  .where(await ops.eq(usersTable.email, "[email protected]"))

// JSONB queries on encrypted JSON columns
const jsonResults = await db.select().from(usersTable)
  .where(await ops.jsonbPathExists(usersTable.profile, "$.bio"))

Drizzle encryptedType Config Options

| Option | Type | Description | |---|---|---| | dataType | "string" | "number" | "json" | Plaintext data type (default: "string") | | equality | boolean | TokenFilter[] | Enable equality index | | freeTextSearch | boolean | MatchIndexOpts | Enable free-text search index | | orderAndRange | boolean | Enable ORE index for sorting/range queries | | searchableJson | boolean | Enable JSONB path queries (requires dataType: "json") |

Drizzle JSONB Operators

For columns with searchableJson: true, three JSONB operators are available:

| Operator | Description | |---|---| | jsonbPathExists(col, selector) | Check if a JSONB path exists (boolean, use in WHERE) | | jsonbPathQueryFirst(col, selector) | Extract first value at a JSONB path | | jsonbGet(col, selector) | Get value using the JSONB -> operator |

These operators encrypt the JSON path selector using the steVecSelector query type and cast it to eql_v2_encrypted for use with the EQL PostgreSQL functions.

Identity-Aware Encryption

Lock encryption to a specific user by requiring a valid JWT for decryption.

import { LockContext } from "@cipherstash/stack/identity"

// 1. Create a lock context (defaults to the "sub" claim)
const lc = new LockContext()

// 2. Identify the user with their JWT
const identifyResult = await lc.identify(userJwt)

if (identifyResult.failure) {
  throw new Error(identifyResult.failure.message)
}

const lockContext = identifyResult.data

// 3. Encrypt with lock context
const encrypted = await client
  .encrypt("sensitive data", { column: users.email, table: users })
  .withLockContext(lockContext)

// 4. Decrypt with the same lock context
const decrypted = await client
  .decrypt(encrypted.data)
  .withLockContext(lockContext)

Lock contexts work with all operations: encrypt, decrypt, encryptModel, decryptModel, bulkEncryptModels, bulkDecryptModels, bulkEncrypt, bulkDecrypt.

Secrets Management

The Secrets class provides end-to-end encrypted secret storage. Values are encrypted locally before being sent to the CipherStash API.

import { Secrets } from "@cipherstash/stack/secrets"

const secrets = new Secrets({
  workspaceCRN: process.env.CS_WORKSPACE_CRN!,
  clientId: process.env.CS_CLIENT_ID!,
  clientKey: process.env.CS_CLIENT_KEY!,
  apiKey: process.env.CS_CLIENT_ACCESS_KEY!,
  environment: "production",
})

// Store a secret
await secrets.set("DATABASE_URL", "postgres://user:pass@host:5432/db")

// Retrieve and decrypt a single secret
const result = await secrets.get("DATABASE_URL")
if (!result.failure) {
  console.log(result.data) // "postgres://user:pass@host:5432/db"
}

// Retrieve multiple secrets in one call
const many = await secrets.getMany(["DATABASE_URL", "API_KEY"])
if (!many.failure) {
  console.log(many.data.DATABASE_URL)
  console.log(many.data.API_KEY)
}

// List secret names (values stay encrypted)
const list = await secrets.list()

// Delete a secret
await secrets.delete("DATABASE_URL")

CLI Reference

The stash CLI is bundled with the package and available after install.

npx stash secrets set  -name DATABASE_URL -value "postgres://..." -environment production
npx stash secrets get  -name DATABASE_URL -environment production
npx stash secrets list -environment production
npx stash secrets delete -name DATABASE_URL -environment production

Commands

| Command | Flags | Aliases | Description | |-----|----|-----|-------| | stash secrets set | -name, -value, -environment | -n, -V, -e | Encrypt and store a secret | | stash secrets get | -name, -environment | -n, -e | Retrieve and decrypt a secret | | stash secrets list | -environment | -e | List all secret names in an environment | | stash secrets delete | -name, -environment, -yes | -n, -e, -y | Delete a secret (prompts for confirmation unless -yes) |

The CLI reads credentials from the same CS_* environment variables described in Configuration.

Configuration

Environment Variables

| Variable | Description | |-----|-------| | CS_WORKSPACE_CRN | The workspace identifier (CRN format) | | CS_CLIENT_ID | The client identifier | | CS_CLIENT_KEY | Client key material used with ZeroKMS for encryption | | CS_CLIENT_ACCESS_KEY | API key for authenticating with the CipherStash API |

Store these in a .env file or set them in your hosting platform.

Sign up at cipherstash.com/signup and follow the onboarding to generate credentials.

TOML Config

You can also configure credentials via cipherstash.toml and cipherstash.secret.toml files in your project root. See the CipherStash docs for format details.

Programmatic Config

Pass config directly when initializing the client:

import { Encryption } from "@cipherstash/stack"
import { users } from "./schema"

const client = await Encryption({
  schemas: [users],
  config: {
    workspaceCrn: "crn:ap-southeast-2.aws:your-workspace-id",
    clientId: "your-client-id",
    clientKey: "your-client-key",
    accessKey: "your-access-key",
    keyset: { name: "my-keyset" }, // or { id: "uuid" }
  },
})

Multi-Tenant Encryption (Keysets)

Isolate encryption keys per tenant using keysets:

const client = await Encryption({
  schemas: [users],
  config: {
    keyset: { id: "123e4567-e89b-12d3-a456-426614174000" },
  },
})

// or by name
const client2 = await Encryption({
  schemas: [users],
  config: {
    keyset: { name: "Company A" },
  },
})

Logging

The SDK uses structured logging across all interfaces (Encryption, Secrets, Supabase, DynamoDB). Each operation emits a single wide event with context such as the operation type, table, column, lock context status, and duration.

Configure the log level with the STASH_STACK_LOG environment variable:

STASH_STACK_LOG=error  # debug | info | error (default: error)

| Value | What is logged | | ------- | ---------------------- | | error | Errors only (default) | | info | Info and errors | | debug | Debug, info, and errors |

When STASH_STACK_LOG is not set, the SDK defaults to error (errors only).

The SDK never logs plaintext data.

Error Handling

All async methods return a Result object with either a data key (success) or a failure key (error). This is a discriminated union - you never get both.

const result = await client.encrypt("hello", { column: users.email, table: users })

if (result.failure) {
  // result.failure.type: string (e.g. "EncryptionError")
  // result.failure.message: string
  console.error(result.failure.type, result.failure.message)
} else {
  // result.data: Encrypted payload
  console.log(result.data)
}

Error Types

| Type | When | |---|---| | ClientInitError | Client initialization fails (bad credentials, missing config) | | EncryptionError | An encrypt operation fails | | DecryptionError | A decrypt operation fails | | LockContextError | Lock context creation or usage fails | | CtsTokenError | Identity token exchange fails |

API Reference

Encryption(config) - Initialize the client

function Encryption(config: EncryptionClientConfig): Promise<EncryptionClient>

EncryptionClient Methods

| Method | Signature | Returns | |----|------|-----| | encrypt | (plaintext, { column, table }) | EncryptOperation (thenable) | | decrypt | (encryptedData) | DecryptOperation (thenable) | | encryptQuery | (plaintext, { column, table, queryType?, returnType? }) | EncryptQueryOperation (thenable) | | encryptQuery | (terms: ScalarQueryTerm[]) | BatchEncryptQueryOperation (thenable) | | encryptModel | (model, table) | EncryptModelOperation<EncryptedFromSchema<T, S>> (thenable) | | decryptModel | (encryptedModel) | DecryptModelOperation<T> (thenable) | | bulkEncrypt | (plaintexts, { column, table }) | BulkEncryptOperation (thenable) | | bulkDecrypt | (encryptedPayloads) | BulkDecryptOperation (thenable) | | bulkEncryptModels | (models, table) | BulkEncryptModelsOperation<EncryptedFromSchema<T, S>> (thenable) | | bulkDecryptModels | (encryptedModels) | BulkDecryptModelsOperation<T> (thenable) |

All operations are thenable (awaitable) and support .withLockContext(lockContext) for identity-aware encryption.

LockContext

import { LockContext } from "@cipherstash/stack/identity"

const lc = new LockContext(options?)
const result = await lc.identify(jwtToken)

Secrets

import { Secrets } from "@cipherstash/stack/secrets"

const secrets = new Secrets(config)
await secrets.set(name, value)
await secrets.get(name)
await secrets.getMany(names)
await secrets.list()
await secrets.delete(name)

Schema Builders

import { encryptedTable, encryptedColumn, csValue } from "@cipherstash/stack/schema"

encryptedTable(tableName, columns)
encryptedColumn(columnName)        // returns EncryptedColumn
csValue(valueName)                 // returns ProtectValue (for nested values)

Subpath Exports

| Import Path | Provides | |-------|-----| | @cipherstash/stack | Encryption function (main entry point) | | @cipherstash/stack/schema | encryptedTable, encryptedColumn, csValue, schema types | | @cipherstash/stack/identity | LockContext class and identity types | | @cipherstash/stack/secrets | Secrets class and secrets types | | @cipherstash/stack/client | Client-safe exports (schema builders and types only - no native FFI) | | @cipherstash/stack/types | All TypeScript types (Encrypted, Decrypted, ClientConfig, EncryptionClientConfig, query types, etc.) |

Migration from @cipherstash/protect

If you are migrating from @cipherstash/protect, the following table maps the old API to the new one:

| @cipherstash/protect | @cipherstash/stack | Import Path | |------------|-----------|-------| | protect(config) | Encryption(config) | @cipherstash/stack | | csTable(name, cols) | encryptedTable(name, cols) | @cipherstash/stack/schema | | csColumn(name) | encryptedColumn(name) | @cipherstash/stack/schema | | import { LockContext } from "@cipherstash/protect/identify" | import { LockContext } from "@cipherstash/stack/identity" | @cipherstash/stack/identity | | N/A | Secrets class | @cipherstash/stack/secrets | | N/A | stash CLI | npx stash |

All method signatures on the encryption client (encrypt, decrypt, encryptModel, etc.) remain the same. The Result pattern (data / failure) is unchanged.

Requirements

  • Node.js >= 18
  • The package includes a native FFI module (@cipherstash/protect-ffi) written in Rust and embedded via Neon. You must opt out of bundling this package in tools like Webpack, esbuild, or Next.js (serverExternalPackages).

License

MIT - see LICENSE.md.