Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .changeset/remove-eql-v2-scaffold-examples-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'stash': patch
'@cipherstash/stack': patch
---

De-suffix the v3 client name in generated code and shipped guidance.

`stash init` scaffolded `import { EncryptionV3 } from '@cipherstash/stack/v3'`
into the client file it writes. `EncryptionV3` is a deprecated alias of
`Encryption`, so new projects were started on the deprecated name. The
scaffold now emits `Encryption`.

`@cipherstash/stack/v3` now re-exports `Encryption` alongside the deprecated
`EncryptionV3` alias, so a v3 schema and its client come from one import
specifier — the deprecation notice already documented this import, but it did
not resolve.

Corrects the bundled agent skills and package docs, which described
`encryptedSupabase` as the legacy EQL v2 wrapper. It is the EQL v3 factory;
the v2 wrapper was removed. Also drops the stale "DynamoDB still requires v2"
note from the `@cipherstash/stack` README — DynamoDB writes EQL v3 and reads
existing v2 items.
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ jobs:
- name: Typecheck (wizard)
run: pnpm --filter @cipherstash/wizard run typecheck

# `examples/*` are standalone apps outside the `./packages/*` filter that
# root `build`/`test` use, so nothing in CI compiled them. `examples/basic`
# had been broken since the v2 removal deleted `encryptedType` and the v2
# `encryptedSupabase` — on a fully green board. Gate it through turbo so
# `^build` builds stack/stack-drizzle/stash first.
- name: Typecheck (examples/basic — guards the v3 stack/stack-drizzle importers)
run: pnpm exec turbo run typecheck --filter @cipherstash/basic-example

- name: Lint — no hardcoded package-manager runners
run: pnpm run lint:runners

Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ If these variables are missing, tests that require live encryption will fail or
- `packages/migrate`: Plaintext-to-encrypted column migration (`@cipherstash/migrate`) — resumable backfill, per-column state
- `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres. **EQL v3 only**: per-domain constructors (`cipherstash.TextSearch()` / `text()` / `bigIntOrd()` / …) and `cipherstashFromStack` (the `./v3` and `./stack` entries). The EQL v2 surface was removed — the adapter's baseline migration installs the EQL v3 bundle only (works on Supabase as a non-superuser)
- `packages/stack-drizzle`: Drizzle ORM integration (`@cipherstash/stack-drizzle`), depends on `@cipherstash/stack` — **EQL v3 only**, on the package root (the v2 surface was removed and the old `./v3` subpath collapsed into `.`). Split out of `@cipherstash/stack`.
- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3). Split out of `@cipherstash/stack`.
- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — **EQL v3 only**: `encryptedSupabase` is the v3 factory (`encryptedSupabaseV3` remains as a `@deprecated` alias). Split out of `@cipherstash/stack`.
- `packages/nextjs`: Next.js helpers and Clerk integration (`./clerk` export)
- `packages/utils`: Shared config (`utils/config`) and logger (`utils/logger`)
- `packages/bench`: Performance / index-engagement benchmarks (private, not published)
Expand Down Expand Up @@ -155,7 +155,7 @@ Three rules to remember when editing CI or pnpm config:
- **Identity-aware encryption**: Authenticate the client as the end user with `OidcFederationStrategy` (`config.authStrategy`, re-exported from `@cipherstash/stack`), then chain `.withLockContext({ identityClaim })` on operations to bind the data key to a claim. The same claim must be used for encrypt and decrypt. (`LockContext.identify()` from `@cipherstash/stack/identity` is deprecated — the strategy now handles token acquisition; `.withLockContext()` also accepts a `LockContext`.)
- **Integrations**:
- **Drizzle ORM**: `types.*` column factories, `extractEncryptionSchema`, `createEncryptionOperators` from `@cipherstash/stack-drizzle`
- **Supabase**: `encryptedSupabase` (v2) / `encryptedSupabaseV3` (v3) from `@cipherstash/stack-supabase`
- **Supabase**: `encryptedSupabase` from `@cipherstash/stack-supabase` (EQL v3; `encryptedSupabaseV3` is a `@deprecated` alias)
- **DynamoDB**: `encryptedDynamoDB` from `@cipherstash/stack/dynamodb`

## Critical Gotchas (read before coding)
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
**Encryption**

```typescript
import { Encryption, encryptedTable, encryptedColumn } from "@cipherstash/stack";
import { Encryption, encryptedTable, types } from "@cipherstash/stack/v3";

// 1. Define your schema
// 1. Define your schema — the column type fixes its query capabilities
const users = encryptedTable("users", {
email: encryptedColumn("email").equality().freeTextSearch(),
email: types.TextSearch("email"), // equality + order/range + free-text search
});

// 2. Initialize the client
Expand Down Expand Up @@ -67,7 +67,7 @@ bun add @cipherstash/stack
## Features

- **[Searchable encryption](https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption)**: query encrypted data with equality, free text search, range, and [JSONB queries](https://cipherstash.com/docs/stack/cipherstash/encryption/searchable-encryption#jsonb-queries-with-searchablejson).
- **[Type-safe schema](https://cipherstash.com/docs/stack/cipherstash/encryption/schema)**: define encrypted tables and columns with `encryptedTable` / `encryptedColumn`
- **[Type-safe schema](https://cipherstash.com/docs/stack/cipherstash/encryption/schema)**: define encrypted tables and columns with `encryptedTable` and the `types.*` concrete-domain factories
- **[Model & bulk operations](https://cipherstash.com/docs/stack/cipherstash/encryption/encrypt-decrypt#model-operations)**: encrypt and decrypt entire objects or batches with `encryptModel` / `bulkEncryptModels`.
- **[Identity-aware encryption](https://cipherstash.com/docs/stack/cipherstash/encryption/identity)**: authenticate as the end user with `OidcFederationStrategy` and bind the data key to their identity with `.withLockContext({ identityClaim })` for policy-based access control.

Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ This repository is the CipherStash Stack monorepo for JavaScript/TypeScript. It
| `@cipherstash/nextjs` | Next.js helpers |
| `@cipherstash/migrate` | Plaintext-to-encrypted column migration tooling |
| `@cipherstash/prisma-next` | Prisma Next integration (searchable field-level encryption for Postgres) |
| `@cipherstash/stack-drizzle` | Drizzle ORM integration for `@cipherstash/stack` (EQL v2 + v3) |
| `@cipherstash/stack-supabase` | Supabase integration for `@cipherstash/stack` (EQL v2 + v3) |
| `@cipherstash/stack-drizzle` | Drizzle ORM integration for `@cipherstash/stack` (EQL v3) |
| `@cipherstash/stack-supabase` | Supabase integration for `@cipherstash/stack` (EQL v3) |
| `@cipherstash/wizard` | AI-powered encryption setup |

**Security fixes are released for the latest release line of each package.** Security reports are welcome for any version, but fixes land in the latest release — if you are running an older major version, plan to upgrade to receive them.
Expand Down
38 changes: 20 additions & 18 deletions docs/reference/supabase-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,41 @@
are transparently encrypted on mutations, `::jsonb`-cast on selects, encrypted
in filter terms, and decrypted in results.

Two entry points, one query mechanism:
One entry point, EQL v3 only:

| Entry point | Schema DSL | Column storage |
|---|---|---|
| `encryptedSupabase` | `@cipherstash/stack/schema` (EQL v2) | `eql_v2_encrypted` composite |
| `encryptedSupabaseV3` | `@cipherstash/stack/eql/v3` (EQL v3) | native `public.eql_v3_*` domains |
| `encryptedSupabase` | `@cipherstash/stack/eql/v3` (EQL v3) | native `public.eql_v3_*` domains |

Both filter via **direct EQL operators over PostgREST**: the wrapper encrypts
`encryptedSupabaseV3` remains as a `@deprecated`, type-identical alias. The old
EQL v2 authoring wrapper — `encryptedSupabase({ encryptionClient,
supabaseClient })` — has been removed; the name now binds to the v3 factory
below.

It filters via **direct EQL operators over PostgREST**: the wrapper encrypts
the filter term and emits an ordinary `col <op> term` filter, which resolves
to the custom operator defined on the encrypted type (equality by HMAC, range
by the ordering term — CLLW-OPE on `_ord` domains, block-ORE on `_ord_ore` —
free-text by bloom-filter containment).

## Quick start (EQL v3)

`encryptedSupabaseV3` is an async factory that **introspects the database at
`encryptedSupabase` is an async factory that **introspects the database at
connect time**: it detects EQL v3 columns by their Postgres domain, derives
each column's encryption config from the domain, and builds the encryption
client internally. Introspection needs a direct Postgres connection
(`options.databaseUrl`, defaulting to `DATABASE_URL`), so the factory cannot
run in a Worker or the browser.

```typescript
import { encryptedSupabaseV3 } from '@cipherstash/stack-supabase'
import { encryptedSupabase } from '@cipherstash/stack-supabase'

// Introspects the database via options.databaseUrl or DATABASE_URL
const es = await encryptedSupabaseV3(
const es = await encryptedSupabase(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
)
// or wrap an existing client: await encryptedSupabaseV3(supabaseClient, options)
// or wrap an existing client: await encryptedSupabase(supabaseClient, options)

await es.from('users').insert({ email: 'a@b.com', amount: 30 })

Expand All @@ -49,16 +53,14 @@ await es.from('users').select('id, amount').gte('amount', 10).lte('amount', 100)
`from(tableName)` takes only the table name — no schema argument; column
capabilities come from the introspected domains.

The builder surface is shared across v2 and v3:
`.select/.insert/.update/.upsert/.delete`,
The builder surface is `.select/.insert/.update/.upsert/.delete`,
`.eq/.neq/.in/.is/.gt/.gte/.lt/.lte/.match/.or/.not/.filter`,
transforms (`.order/.limit/.range/.single/.maybeSingle/.csv/.abortSignal/.throwOnError`),
plus `.withLockContext(lockContext)` and `.audit(config)` — with one fork:
free-text search. v2 exposes `.like/.ilike` (SQL wildcard matching); v3
exposes `.matches()` (fuzzy bloom token search) on encrypted columns, keeps
`.contains()` for native (exact) containment on plaintext columns, and treats
`like`/`ilike` on an encrypted column as an approximate shim that delegates to
`.matches()` (see "v3 encoding details" below).
plus `.withLockContext(lockContext)` and `.audit(config)`. For free-text
search it exposes `.matches()` (fuzzy bloom token search) on encrypted
columns, keeps `.contains()` for native (exact) containment on plaintext
columns, and treats `like`/`ilike` on an encrypted column as an approximate
shim that delegates to `.matches()` (see "v3 encoding details" below).

### Typing (v3)

Expand All @@ -68,14 +70,14 @@ tables against the database at construction:

```typescript
import { encryptedTable, types } from '@cipherstash/stack/eql/v3'
import { encryptedSupabaseV3 } from '@cipherstash/stack-supabase'
import { encryptedSupabase } from '@cipherstash/stack-supabase'

const users = encryptedTable('users', {
email: types.TextSearch('email'), // public.eql_v3_text_search
amount: types.IntegerOrd('amount'), // public.eql_v3_integer_ord
})

const es = await encryptedSupabaseV3(
const es = await encryptedSupabase(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ schemas: { users } },
Expand Down
8 changes: 6 additions & 2 deletions examples/basic/encrypt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'dotenv/config'

import { Encryption, encryptedColumn, encryptedTable } from '@cipherstash/stack'
import { Encryption, encryptedTable, types } from '@cipherstash/stack/v3'

// EQL v3: a column's query capabilities are fixed by the domain you pick —
// there are no chainable capability tuners. `types.Text` is storage-only
// (encrypt/decrypt, no queries), which is all this demo needs. Reach for
// `types.TextEq` / `types.TextSearch` when you need to query the column.
export const users = encryptedTable('users', {
name: encryptedColumn('name'),
name: types.Text('name'),
})

export const client = await Encryption({
Expand Down
27 changes: 0 additions & 27 deletions examples/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dotenv/config'
import readline from 'node:readline'
import { client, users } from './encrypt'
import { createContact, getAllContacts } from './src/queries/contacts'

const rl = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -69,32 +68,6 @@ async function main() {

console.log('Bulk encrypted data:', bulkEncryptResult.data)

// Demonstrate Supabase integration with CipherStash encryption
console.log('\n--- Supabase Integration Demo ---')

try {
// Example: Create a new contact (would insert into encrypted Supabase table)
console.log('Creating encrypted contact...')
const newContact = {
name: 'John Doe',
email: 'john@example.com',
role: 'Developer', // This field will be encrypted using CipherStash
}

// Note: This would fail in this basic example since we don't have actual Supabase setup
// but shows the pattern for encrypted Supabase usage
console.log('Contact data to encrypt:', newContact)

// Example: Fetch contacts (would decrypt results from Supabase)
console.log('Fetching encrypted contacts...')
// const contacts = await getAllContacts()
// console.log('Decrypted contacts:', contacts.data)
} catch (error) {
console.log(
'Supabase demo skipped (no actual Supabase connection in this basic example)',
)
}

rl.close()
}

Expand Down
5 changes: 3 additions & 2 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"version": "1.2.14-rc.4",
"type": "module",
"scripts": {
"start": "tsx index.ts"
"start": "tsx index.ts",
"typecheck": "tsc --project tsconfig.json --noEmit"
},
"keywords": [],
"author": "",
Expand All @@ -13,7 +14,7 @@
"dependencies": {
"@cipherstash/stack": "workspace:*",
"@cipherstash/stack-drizzle": "workspace:*",
"@cipherstash/stack-supabase": "workspace:*",
"drizzle-orm": "^0.45.2",
"dotenv": "^17.4.2",
"pg": "8.22.0"
},
Expand Down
21 changes: 8 additions & 13 deletions examples/basic/src/encryption/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { Encryption } from '@cipherstash/stack'
import {
encryptedType,
extractEncryptionSchema,
} from '@cipherstash/stack-drizzle'
import { Encryption } from '@cipherstash/stack/v3'
import { extractEncryptionSchema, types } from '@cipherstash/stack-drizzle'
import { integer, pgTable, timestamp } from 'drizzle-orm/pg-core'

// EQL v3 encrypted columns are concrete Postgres domains built with the
// `types.*` factories. The domain fixes the query capabilities: `TextSearch`
// is equality + order/range + free-text, the v3 equivalent of what the old v2
// builder spelled `.equality().freeTextSearch()`.
export const usersTable = pgTable('users', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
email: encryptedType<string>('email', {
equality: true,
freeTextSearch: true,
}),
name: encryptedType<string>('name', {
equality: true,
freeTextSearch: true,
}),
email: types.TextSearch('email'),
name: types.TextSearch('name'),
createdAt: timestamp('created_at').defaultNow(),
})

Expand Down
9 changes: 0 additions & 9 deletions examples/basic/src/lib/supabase/encrypted.ts

This file was deleted.

8 changes: 0 additions & 8 deletions examples/basic/src/lib/supabase/server.ts

This file was deleted.

68 changes: 0 additions & 68 deletions examples/basic/src/queries/contacts.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
Performance / index-engagement benchmarks for stack integrations.

This package validates that each integration emits SQL that engages the canonical
EQL functional indexes (`eql_v2.hmac_256`, `eql_v2.bloom_filter`, `eql_v2.ste_vec`)
on a Supabase-shaped install (no operator classes). It runs in two layers:
EQL functional indexes (`eql_v3.eq_term`, `eql_v3.match_term`,
`eql_v3.to_ste_vec_query`) on a Supabase-shaped install (no operator classes).
It runs in two layers:

1. **EXPLAIN-shape tests** (`__tests__/`) — vitest tests that assert on
`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` output. Pass/fail. Cheap.
Expand Down
Loading