Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
</a>
<h1>CipherStash Stack for TypeScript</h1>

<p><strong>Data-level access control for TypeScript.</strong><br/>Every sensitive value encrypted with a unique key. Searchable on existing Postgres indexes.<br/>A breach yields ciphertext, nothing useful.</p>

<a href="https://cipherstash.com"><img alt="Built by CipherStash" src="https://raw.githubusercontent.com/cipherstash/meta/refs/heads/main/csbadge.svg?style=for-the-badge&labelColor=000"></a>
<a href="https://github.com/cipherstash/stack/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/npm/l/@cipherstash/stack.svg?style=for-the-badge&labelColor=000000"></a>
<a href="https://cipherstash.com/docs"><img alt="Docs" src="https://img.shields.io/badge/Docs-333333.svg?style=for-the-badge&logo=readthedocs&labelColor=333"></a>
Expand All @@ -13,7 +15,9 @@

## What is the stack?

- [Encryption](https://cipherstash.com/docs/stack/cipherstash/encryption): Field-level encryption for TypeScript apps with searchable encrypted queries, zero-knowledge key management, and first-class ORM support.
CipherStash makes access control cryptographic. The rules aren't configured — they're enforced. Every sensitive value carries a decryption policy that travels with the data, wherever it ends up: past the API response, past an agent tool call, past the database. The stack is the TypeScript surface to that model.

- [Encryption](https://cipherstash.com/docs/stack/cipherstash/encryption): Searchable, application-layer field-level encryption for TypeScript apps. Range queries, exact match, and free-text fuzzy search over encrypted fields with sub-millisecond overhead on existing Postgres indexes. Identity-bound keys via `LockContext`. First-class ORM support.

## Quick look at the stack in action

Expand Down Expand Up @@ -79,8 +83,10 @@ bun add @cipherstash/stack

## Use cases

- **Trusted data access**: ensure only your end-users can access their sensitive data using identity-bound encryption
- **Reduce breach impact**: limit the blast radius of exploited vulnerabilities to only the data the affected user can decrypt
- **A breach yields ciphertext, nothing useful** — limit the blast radius of compromised credentials and exploited vulnerabilities to the data the attacker's identity can decrypt.
- **Per-value access policy** — enforce who can decrypt what, wherever the data ends up.
- **Agent-safe by design** — sensitive values stay encrypted through agent tool calls and downstream pipelines until the right identity asks for them.
- **Faster, simpler, and more reliable than row-level security** — the policy travels with the data, not the database connection.

## Documentation

Expand Down
8 changes: 7 additions & 1 deletion packages/stack/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# @cipherstash/stack

The all-in-one TypeScript SDK for the CipherStash data security stack.
**Data-level access control for TypeScript.** Every sensitive value encrypted with a unique key. Identity-bound, searchable, and built into your existing Postgres stack.

[![npm version](https://img.shields.io/npm/v/@cipherstash/stack.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@cipherstash/stack)
[![License: MIT](https://img.shields.io/npm/l/@cipherstash/stack.svg?style=for-the-badge&labelColor=000000)](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md)
[![TypeScript](https://img.shields.io/badge/TypeScript-first-blue?style=for-the-badge&labelColor=000000)](https://www.typescriptlang.org/)

--

CipherStash makes access control cryptographic. The rules aren't configured — they're enforced. Every value is encrypted under a unique key with identity and policy baked in, decryption enforced at the moment of read. A breach yields ciphertext.

This SDK is the TypeScript surface: searchable field-level encryption, identity-bound keys via `LockContext`, bulk operations against [ZeroKMS](https://cipherstash.com/products/zerokms), and first-class integrations for Drizzle, Supabase, Prisma, and DynamoDB.

--

## Table of Contents

- [Install](#install)
Expand Down
81 changes: 81 additions & 0 deletions packages/stack/__tests__/prisma-batcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createBatcher } from '@/prisma/core/batcher'
import { describe, expect, it, vi } from 'vitest'

describe('createBatcher', () => {
it('coalesces synchronous enqueues into a single flush call', async () => {
const flush = vi.fn(async (values: readonly number[]) =>
values.map((v) => v * 2),
)
const batcher = createBatcher<number, number>(flush)

// This is the shape of `Promise.all(values.map(codec.encode))` —
// every enqueue runs synchronously before the first microtask fires.
const results = await Promise.all([
batcher.enqueue(1),
batcher.enqueue(2),
batcher.enqueue(3),
batcher.enqueue(4),
])

expect(flush).toHaveBeenCalledTimes(1)
expect(flush.mock.calls[0]?.[0]).toEqual([1, 2, 3, 4])
expect(results).toEqual([2, 4, 6, 8])
})

it('starts a fresh batch after the previous drain resolves', async () => {
const flush = vi.fn(async (values: readonly string[]) => values.slice())
const batcher = createBatcher<string, string>(flush)

await Promise.all([batcher.enqueue('a'), batcher.enqueue('b')])
await Promise.all([batcher.enqueue('c'), batcher.enqueue('d')])

expect(flush).toHaveBeenCalledTimes(2)
expect(flush.mock.calls[0]?.[0]).toEqual(['a', 'b'])
expect(flush.mock.calls[1]?.[0]).toEqual(['c', 'd'])
})

it('rejects every queued promise when the flush throws', async () => {
const error = new Error('flush failure')
const batcher = createBatcher<number, number>(async () => {
throw error
})

const results = await Promise.allSettled([
batcher.enqueue(1),
batcher.enqueue(2),
])

expect(results).toEqual([
{ status: 'rejected', reason: error },
{ status: 'rejected', reason: error },
])
})

it('rejects every queued promise when the flush returns the wrong number of results', async () => {
const batcher = createBatcher<number, number>(async () => [99]) // length mismatch

const results = await Promise.allSettled([
batcher.enqueue(1),
batcher.enqueue(2),
])

expect(results.every((r) => r.status === 'rejected')).toBe(true)
})

it('preserves insertion order across all queued entries', async () => {
const flush = vi.fn(async (values: readonly string[]) => values.slice())
const batcher = createBatcher<string, string>(flush)

const results = await Promise.all([
batcher.enqueue('a'),
batcher.enqueue('b'),
batcher.enqueue('c'),
batcher.enqueue('d'),
batcher.enqueue('e'),
])

expect(flush).toHaveBeenCalledTimes(1)
expect(flush.mock.calls[0]?.[0]).toEqual(['a', 'b', 'c', 'd', 'e'])
expect(results).toEqual(['a', 'b', 'c', 'd', 'e'])
})
})
Loading
Loading