From 5fab1cf6a76f220619fc8e9a709fe89e19e424d9 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 24 Jul 2026 11:22:05 +1000 Subject: [PATCH 1/2] feat(cli,examples)!: de-suffix the init scaffold, rewrite examples/basic to v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs 3-5 of the EQL v2 removal froze the target names, but nothing type-checks the artefacts that describe them, so two were left wrong. `stash init` scaffolded `EncryptionV3` into customer source. That name is a deprecated alias now, and the scaffold is a template literal, so only an assertion can catch it — the codegen tests pinned the old form, so they are flipped with negatives added, mirroring the drizzle precedent from 7b783eef. `@cipherstash/stack/v3` exported `EncryptionV3` but not `Encryption`, so the scaffold could not emit a single clean import. Re-export it — the deprecation example in v3.ts already told users to import it from there. `examples/basic` had not compiled since the removal deleted `encryptedType` and the v2 `encryptedSupabase`. Ported to the v3 types.* factories. Its Supabase branch is deleted rather than ported: it imported a `contactsTable` that was never exported, so it was already dead before this work, and examples/supabase-worker carries the Supabase story. Root `build`/`test` filter to ./packages/*, so CI never compiled any example and the breakage sat on a green board. Gate examples/basic through a new turbo `typecheck` task so `^build` builds its deps first. Verified the gate fails on the exact regression that shipped. --- .github/workflows/tests.yml | 8 +++ examples/basic/encrypt.ts | 8 ++- examples/basic/index.ts | 27 -------- examples/basic/package.json | 5 +- examples/basic/src/encryption/index.ts | 21 +++--- examples/basic/src/lib/supabase/encrypted.ts | 9 --- examples/basic/src/lib/supabase/server.ts | 8 --- examples/basic/src/queries/contacts.ts | 68 ------------------- .../init/__tests__/utils-codegen.test.ts | 8 ++- .../src/commands/init/steps/install-eql.ts | 4 +- packages/cli/src/commands/init/utils.ts | 34 +++++----- packages/stack/src/encryption/v3.ts | 5 +- pnpm-lock.yaml | 6 +- turbo.json | 4 ++ 14 files changed, 62 insertions(+), 153 deletions(-) delete mode 100644 examples/basic/src/lib/supabase/encrypted.ts delete mode 100644 examples/basic/src/lib/supabase/server.ts delete mode 100644 examples/basic/src/queries/contacts.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1b96cbb2f..849feafbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/examples/basic/encrypt.ts b/examples/basic/encrypt.ts index 140e22605..17f869fd5 100644 --- a/examples/basic/encrypt.ts +++ b/examples/basic/encrypt.ts @@ -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({ diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 63bc282a6..b00ad13fa 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -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, @@ -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() } diff --git a/examples/basic/package.json b/examples/basic/package.json index 1c1de60b3..4a62550ad 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -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": "", @@ -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" }, diff --git a/examples/basic/src/encryption/index.ts b/examples/basic/src/encryption/index.ts index 4f78be08e..fca33a2bf 100644 --- a/examples/basic/src/encryption/index.ts +++ b/examples/basic/src/encryption/index.ts @@ -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('email', { - equality: true, - freeTextSearch: true, - }), - name: encryptedType('name', { - equality: true, - freeTextSearch: true, - }), + email: types.TextSearch('email'), + name: types.TextSearch('name'), createdAt: timestamp('created_at').defaultNow(), }) diff --git a/examples/basic/src/lib/supabase/encrypted.ts b/examples/basic/src/lib/supabase/encrypted.ts deleted file mode 100644 index 147930987..000000000 --- a/examples/basic/src/lib/supabase/encrypted.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { encryptedSupabase } from '@cipherstash/stack-supabase' -import { contactsTable, encryptionClient } from '../../encryption/index' -import { createServerClient } from './server' - -const supabase = await createServerClient() -export const eSupabase = encryptedSupabase({ - encryptionClient, - supabaseClient: supabase, -}) diff --git a/examples/basic/src/lib/supabase/server.ts b/examples/basic/src/lib/supabase/server.ts deleted file mode 100644 index 967949176..000000000 --- a/examples/basic/src/lib/supabase/server.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createClient } from '@supabase/supabase-js' - -export async function createServerClient() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! - const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - - return createClient(supabaseUrl, supabaseKey) -} diff --git a/examples/basic/src/queries/contacts.ts b/examples/basic/src/queries/contacts.ts deleted file mode 100644 index ad8979b39..000000000 --- a/examples/basic/src/queries/contacts.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { contactsTable } from '../encryption/index' -import { eSupabase } from '../lib/supabase/encrypted' - -// Example queries using encrypted Supabase wrapper - -export async function getAllContacts() { - const { data, error } = await eSupabase - .from('contacts', contactsTable) - .select('id, name, email, role') // explicit columns, no * - .order('created_at', { ascending: false }) - - return { data, error } -} - -export async function getContactsByRole(role: string) { - const { data, error } = await eSupabase - .from('contacts', contactsTable) - .select('id, name, email, role') - .eq('role', role) // auto-encrypted - - return { data, error } -} - -export async function searchContactsByName(searchTerm: string) { - const { data, error } = await eSupabase - .from('contacts', contactsTable) - .select('id, name, email, role') - .ilike('name', `%${searchTerm}%`) // auto-encrypted - - return { data, error } -} - -export async function createContact(contact: { - name: string - email: string - role: string -}) { - const { data, error } = await eSupabase - .from('contacts', contactsTable) - .insert(contact) // auto-encrypted - .select('id, name, email, role') - .single() - - return { data, error } -} - -export async function updateContact( - id: string, - updates: Partial<{ name: string; email: string; role: string }>, -) { - const { data, error } = await eSupabase - .from('contacts', contactsTable) - .update(updates) // auto-encrypted - .eq('id', id) - .select('id, name, email, role') - .single() - - return { data, error } -} - -export async function deleteContact(id: string) { - const { error } = await eSupabase - .from('contacts', contactsTable) - .delete() - .eq('id', id) - - return { error } -} diff --git a/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts b/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts index 5aabc73d4..59067ee14 100644 --- a/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts +++ b/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts @@ -20,7 +20,12 @@ describe('generateClientFromSchemas', () => { expect(out).toContain("age: types.IntegerOrd('age'),") expect(out).toContain("verified: types.Boolean('verified'),") expect(out).toContain("from '@cipherstash/stack/v3'") - expect(out).toContain('EncryptionV3(') + // `Encryption` is the current name; `EncryptionV3` is a deprecated alias. + // Same reasoning as the drizzle negatives below — this is a template + // literal written into the user's repo as real source, so nothing but an + // assertion here catches a scaffold that teaches the deprecated name. + expect(out).toContain('Encryption(') + expect(out).not.toContain('EncryptionV3') }) it('emits the chosen v3 domain factory per column (drizzle)', () => { @@ -54,6 +59,7 @@ describe('generateClientFromSchemas', () => { const out = generateClientFromSchemas('supabase', schemas) expect(out).toContain("email: types.TextSearch('email'),") expect(out).toContain("from '@cipherstash/stack/v3'") + expect(out).not.toContain('EncryptionV3') expect(out).not.toContain('@cipherstash/stack-drizzle') }) diff --git a/packages/cli/src/commands/init/steps/install-eql.ts b/packages/cli/src/commands/init/steps/install-eql.ts index 9ee6bd880..f6c912466 100644 --- a/packages/cli/src/commands/init/steps/install-eql.ts +++ b/packages/cli/src/commands/init/steps/install-eql.ts @@ -108,8 +108,8 @@ export const installEqlStep: InitStep = { // at all. That pin made `stash init --drizzle` the one flow that provisions // a v2 database while every other integration (and a bare `stash eql // install`) gets v3, and it contradicted the stash-drizzle skill we install - // into the very same project — that skill documents the `/v3` surface - // (`types.*` domains, `EncryptionV3`) and would have the user's agent + // into the very same project — that skill documents the v3 surface + // (`types.*` domains, `Encryption`) and would have the user's agent // author v3 code against a v2 database. // // `stash eql migration --drizzle` (added in #691) closes that gap: v3 SQL, diff --git a/packages/cli/src/commands/init/utils.ts b/packages/cli/src/commands/init/utils.ts index 68526460e..fc0e6aec1 100644 --- a/packages/cli/src/commands/init/utils.ts +++ b/packages/cli/src/commands/init/utils.ts @@ -286,11 +286,11 @@ const ${schemaVarName} = extractEncryptionSchema(${varName})` return `import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core' import { types, extractEncryptionSchema } from '@cipherstash/stack-drizzle' -import { EncryptionV3 } from '@cipherstash/stack/v3' +import { Encryption } from '@cipherstash/stack/v3' ${tableDefs.join('\n\n')} -export const encryptionClient = await EncryptionV3({ +export const encryptionClient = await Encryption({ schemas: [${schemaVarNames.join(', ')}], }) ` @@ -311,11 +311,11 @@ ${columnDefs.join('\n')} const tableVarNames = schemas.map((s) => `${toCamelCase(s.tableName)}Table`) - return `import { EncryptionV3, encryptedTable, types } from '@cipherstash/stack/v3' + return `import { Encryption, encryptedTable, types } from '@cipherstash/stack/v3' ${tableDefs.join('\n\n')} -export const encryptionClient = await EncryptionV3({ +export const encryptionClient = await Encryption({ schemas: [${tableVarNames.join(', ')}], }) ` @@ -366,7 +366,7 @@ export function generateClientFromSchemas( * schemas yet, and explicitly tells the agent that the user's existing * schema files remain authoritative. The agent's job during the handoff * is to declare encrypted columns directly in those files and update the - * `EncryptionV3({ schemas: [...] })` call below to reference them. + * `Encryption({ schemas: [...] })` call below to reference them. */ export function generatePlaceholderClient(integration: Integration): string { if (integration === 'drizzle') { @@ -381,7 +381,7 @@ const DRIZZLE_PLACEHOLDER = `/** * \`stash init\` wrote this file. It is intentionally NOT a real Drizzle * schema. Your existing schema files (typically under \`src/db/\`) remain * authoritative — your agent will edit those directly when you encrypt a - * column, then update the \`EncryptionV3({ schemas: [...] })\` call below + * column, then update the \`Encryption({ schemas: [...] })\` call below * to reference the encrypted tables you declared there. * * Until that happens, the encryption client is initialised with no @@ -419,19 +419,19 @@ const DRIZZLE_PLACEHOLDER = `/** * billing_address: types.TextEq('billing_address'), * }) * - * Once you have encrypted tables declared, harvest them and pass to EncryptionV3(): + * Once you have encrypted tables declared, harvest them and pass to Encryption(): * * import { extractEncryptionSchema } from '@cipherstash/stack-drizzle' - * import { EncryptionV3 } from '@cipherstash/stack/v3' + * import { Encryption } from '@cipherstash/stack/v3' * import { users, orders } from './db/schema' * - * export const encryptionClient = await EncryptionV3({ + * export const encryptionClient = await Encryption({ * schemas: [extractEncryptionSchema(users), extractEncryptionSchema(orders)], * }) */ -import { EncryptionV3 } from '@cipherstash/stack/v3' +import { Encryption } from '@cipherstash/stack/v3' -export const encryptionClient = await EncryptionV3({ schemas: [] }) +export const encryptionClient = await Encryption({ schemas: [] }) ` const GENERIC_PLACEHOLDER = `/** @@ -440,7 +440,7 @@ const GENERIC_PLACEHOLDER = `/** * \`stash init\` wrote this file. It is intentionally NOT a real schema * definition. Your existing schema files remain authoritative — your * agent will declare encrypted columns there and update the - * \`EncryptionV3({ schemas: [...] })\` call below to reference them. + * \`Encryption({ schemas: [...] })\` call below to reference them. * * Until that happens, the encryption client is initialised with no * schemas, and \`stash encrypt\` commands will surface a clear error @@ -474,16 +474,16 @@ const GENERIC_PLACEHOLDER = `/** * billing_address: types.TextEq('billing_address'), * }) * - * Once you have encrypted tables declared, pass them to EncryptionV3(): + * Once you have encrypted tables declared, pass them to Encryption(): * - * import { EncryptionV3 } from '@cipherstash/stack/v3' + * import { Encryption } from '@cipherstash/stack/v3' * import { users, orders } from './db/schema' * - * export const encryptionClient = await EncryptionV3({ + * export const encryptionClient = await Encryption({ * schemas: [users, orders], * }) */ -import { EncryptionV3 } from '@cipherstash/stack/v3' +import { Encryption } from '@cipherstash/stack/v3' -export const encryptionClient = await EncryptionV3({ schemas: [] }) +export const encryptionClient = await Encryption({ schemas: [] }) ` diff --git a/packages/stack/src/encryption/v3.ts b/packages/stack/src/encryption/v3.ts index e36c90731..dc4602e13 100644 --- a/packages/stack/src/encryption/v3.ts +++ b/packages/stack/src/encryption/v3.ts @@ -334,5 +334,8 @@ export const EncryptionV3 = Encryption // Single import surface: re-export the v3 `types` namespace + table API + type // helpers so `@cipherstash/stack/v3` provides everything needed to author and -// use a schema. +// use a schema. `Encryption` comes along for the same reason — it is the +// current name for what `EncryptionV3` aliases, so authoring a v3 schema and +// building its client should not need a second import specifier. +export { Encryption } export * from '@/eql/v3' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46f4b28e7..a2d261a15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,12 +117,12 @@ importers: '@cipherstash/stack-drizzle': specifier: workspace:* version: link:../../packages/stack-drizzle - '@cipherstash/stack-supabase': - specifier: workspace:* - version: link:../../packages/stack-supabase dotenv: specifier: ^17.4.2 version: 17.4.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@types/pg@8.20.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.22.0)(postgres@3.4.9) pg: specifier: 8.22.0 version: 8.22.0 diff --git a/turbo.json b/turbo.json index 4ef8a864b..6ed7b4a76 100644 --- a/turbo.json +++ b/turbo.json @@ -19,6 +19,10 @@ "inputs": ["$TURBO_DEFAULT$", ".env*"], "cache": false }, + "typecheck": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$"] + }, "test:e2e": { "dependsOn": ["^build", "build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], From 52822811d35f86df02fe88f828cdab2d3017e17f Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Fri, 24 Jul 2026 11:31:28 +1000 Subject: [PATCH 2/2] docs(skills,meta): correct the naming the v2 removal inverted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs 3-5 froze the API names but the prose describing them was not swept, so the shipped guidance told users the opposite of what the code does. The worst of it is in skills/, which ship inside the stash tarball and get copied into customer repos: stash-encryption and stash-cli both described `encryptedSupabase` as the legacy EQL v2 wrapper. It is the EQL v3 factory — the v2 wrapper was deleted in #769. stash-drizzle taught `EncryptionV3` as the canonical client, contradicting stash-encryption, which correctly calls it deprecated. Also corrects the @cipherstash/stack README, the npm landing page, which claimed DynamoDB "still requires v2" and pointed at #657 — #768 removed the v2 write overloads and #657 is closed. The root README quickstart still taught the v2 chainable builders as the primary example. Reframes the "Legacy: EQL v2" sections to say what is actually true now: v2 is a read path, not an authoring surface. Left alone deliberately: the v2 CLI/database sections in supabase-sdk.md and the v2 read path in stash-dynamodb — both accurate today, and the CLI text moves with the SQL teardown. `export { Encryption }` in v3.ts is ordered after the `export *` so Biome's organizeImports is satisfied without detaching the comment from its subject. --- .../remove-eql-v2-scaffold-examples-meta.md | 22 +++++++ AGENTS.md | 4 +- README.md | 8 +-- SECURITY.md | 4 +- docs/reference/supabase-sdk.md | 38 ++++++------ packages/bench/README.md | 5 +- packages/migrate/README.md | 2 +- packages/stack-drizzle/README.md | 4 +- packages/stack/README.md | 58 ++++++++++--------- packages/stack/src/encryption/v3.ts | 9 +-- skills/stash-cli/SKILL.md | 2 +- skills/stash-drizzle/SKILL.md | 12 ++-- skills/stash-encryption/SKILL.md | 6 +- skills/stash-prisma-next/SKILL.md | 2 +- 14 files changed, 102 insertions(+), 74 deletions(-) create mode 100644 .changeset/remove-eql-v2-scaffold-examples-meta.md diff --git a/.changeset/remove-eql-v2-scaffold-examples-meta.md b/.changeset/remove-eql-v2-scaffold-examples-meta.md new file mode 100644 index 000000000..38714d679 --- /dev/null +++ b/.changeset/remove-eql-v2-scaffold-examples-meta.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index 5fcc10440..943dbb981 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) @@ -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) diff --git a/README.md b/README.md index 0753d24e1..489c7f83a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/SECURITY.md b/SECURITY.md index 47b725910..ba3996d97 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. diff --git a/docs/reference/supabase-sdk.md b/docs/reference/supabase-sdk.md index 6561f255e..7294a8eb9 100644 --- a/docs/reference/supabase-sdk.md +++ b/docs/reference/supabase-sdk.md @@ -4,14 +4,18 @@ 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 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` — @@ -19,7 +23,7 @@ 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 @@ -27,14 +31,14 @@ client internally. Introspection needs a direct Postgres connection 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 }) @@ -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) @@ -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 } }, diff --git a/packages/bench/README.md b/packages/bench/README.md index af5c3e38b..cd5409815 100644 --- a/packages/bench/README.md +++ b/packages/bench/README.md @@ -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. diff --git a/packages/migrate/README.md b/packages/migrate/README.md index ea1fb6549..30d03a961 100644 --- a/packages/migrate/README.md +++ b/packages/migrate/README.md @@ -43,7 +43,7 @@ Creates `cipherstash.cs_migrations` idempotently. Normally called by `stash eql Chunked, resumable, idempotent backfill of plaintext → encrypted. Per chunk, in a single transaction: select next page → encrypt via `client.bulkEncryptModels` → `UPDATE … FROM (VALUES …)` → `INSERT` a `backfill_checkpoint` event. Guards with `encrypted IS NULL` so re-runs never double-write. - `db`: a `pg.PoolClient` (the runner drives transactions on it). -- `encryptionClient`: your initialised `@cipherstash/stack` client (or anything that exposes `bulkEncryptModels(models, table)` returning `{ data } | { failure }`). For an EQL v3 column pass an `EncryptionV3` client (from `@cipherstash/stack/v3`) — it pins the v3 wire format; the engine itself is version-agnostic and writes whatever envelope the client produces. +- `encryptionClient`: your initialised `@cipherstash/stack` client (or anything that exposes `bulkEncryptModels(models, table)` returning `{ data } | { failure }`). For an EQL v3 column pass an `Encryption` client (from `@cipherstash/stack/v3`) — it pins the v3 wire format; the engine itself is version-agnostic and writes whatever envelope the client produces. - `tableSchema`: the `EncryptedTable` for the target table from your encryption client file. - `signal`: optional `AbortSignal`. If aborted between chunks, the backfill exits cleanly and leaves a resumable checkpoint. diff --git a/packages/stack-drizzle/README.md b/packages/stack-drizzle/README.md index c3596884d..db8a1b45a 100644 --- a/packages/stack-drizzle/README.md +++ b/packages/stack-drizzle/README.md @@ -17,7 +17,7 @@ object. Install the domains once with `stash eql install --eql-version 3`. ```ts import { pgTable, integer } from 'drizzle-orm/pg-core' -import { EncryptionV3 } from '@cipherstash/stack/v3' +import { Encryption } from '@cipherstash/stack/v3' import { types as encryptedTypes, extractEncryptionSchema, @@ -31,7 +31,7 @@ const users = pgTable('users', { }) const schema = extractEncryptionSchema(users) -const client = await EncryptionV3({ schemas: [schema] }) +const client = await Encryption({ schemas: [schema] }) const ops = createEncryptionOperators(client) // Insert — encrypt models first diff --git a/packages/stack/README.md b/packages/stack/README.md index 8ca24e116..942f65fc9 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -57,7 +57,7 @@ The wizard will authenticate you, walk you through choosing a database connectio Define a table with concrete EQL v3 column types, build the typed client, and encrypt: ```typescript -import { EncryptionV3 } from "@cipherstash/stack/v3" +import { Encryption } from "@cipherstash/stack/v3" import { encryptedTable, types } from "@cipherstash/stack/eql/v3" // Define a schema — the column type fixes its query capabilities @@ -66,7 +66,7 @@ const users = encryptedTable("users", { }) // Create a typed client -const client = await EncryptionV3({ schemas: [users] }) +const client = await Encryption({ schemas: [users] }) // Encrypt a value const encrypted = await client.encrypt("hello@example.com", { @@ -374,7 +374,7 @@ import { createEncryptionOperators, extractEncryptionSchema, } from "@cipherstash/stack-drizzle" -import { EncryptionV3 } from "@cipherstash/stack/v3" +import { Encryption } from "@cipherstash/stack/v3" // Capabilities come from the concrete type — no flags to configure. const users = pgTable("users", { @@ -390,7 +390,7 @@ Derive the v3 schema from the table, build the typed client, and create the oper ```ts const usersSchema = extractEncryptionSchema(users) -const client = await EncryptionV3({ schemas: [usersSchema] }) +const client = await Encryption({ schemas: [usersSchema] }) const ops = createEncryptionOperators(client) const db = drizzle({ client: sqlClient }) @@ -478,7 +478,7 @@ explicit strategies cover the other cases: ```typescript import { OidcFederationStrategy } from "@cipherstash/stack" -import { EncryptionV3 } from "@cipherstash/stack/v3" +import { Encryption } from "@cipherstash/stack/v3" // The callback is re-invoked on every (re-)federation and must return the // CURRENT third-party OIDC JWT. @@ -488,7 +488,7 @@ const strategy = OidcFederationStrategy.create( ) if (strategy.failure) throw new Error(strategy.failure.error.message) -const client = await EncryptionV3({ +const client = await Encryption({ schemas: [users], config: { authStrategy: strategy.data }, }) @@ -598,10 +598,10 @@ See the [Going to Production](https://cipherstash.com/docs/stack/deploy/going-to Pass config directly when initializing the client: ```typescript -import { EncryptionV3 } from "@cipherstash/stack/v3" +import { Encryption } from "@cipherstash/stack/v3" import { users } from "./schema" -const client = await EncryptionV3({ +const client = await Encryption({ schemas: [users], config: { workspaceCrn: "crn:ap-southeast-2.aws:your-workspace-id", @@ -618,7 +618,7 @@ const client = await EncryptionV3({ Isolate encryption keys per tenant using keysets: ```typescript -const client = await EncryptionV3({ +const client = await Encryption({ schemas: [users], config: { keyset: { id: "123e4567-e89b-12d3-a456-426614174000" }, @@ -626,7 +626,7 @@ const client = await EncryptionV3({ }) // or by name -const client2 = await EncryptionV3({ +const client2 = await Encryption({ schemas: [users], config: { keyset: { name: "Company A" }, @@ -683,10 +683,10 @@ if (result.failure) { ## API Reference -### `EncryptionV3(config)` - Initialize the typed client +### `Encryption(config)` - Initialize the typed client ```typescript -function EncryptionV3(config: { +function Encryption(config: { schemas: AnyV3Table[] config?: ClientConfig }): Promise @@ -749,9 +749,9 @@ type UserEncrypted = InferEncrypted | Import Path | Provides | |-------|-----| -| `@cipherstash/stack/v3` | `EncryptionV3` typed client factory, `typedClient`, plus re-exports of the EQL v3 authoring DSL | +| `@cipherstash/stack/v3` | `Encryption` typed client factory (`EncryptionV3` is a `@deprecated` alias), `typedClient`, plus re-exports of the EQL v3 authoring DSL | | `@cipherstash/stack/eql/v3` | EQL v3 authoring DSL: `encryptedTable`, the `types` namespace, `buildEncryptConfig`, inference types (`InferPlaintext`, `InferEncrypted`, ...) | -| `@cipherstash/stack` | `Encryption` function (legacy v2 entry point), auth strategies | +| `@cipherstash/stack` | `Encryption` client factory, auth strategies | | `@cipherstash/stack/schema` | Legacy v2 schema builders (see [Legacy: EQL v2](#legacy-eql-v2)) | | `@cipherstash/stack/identity` | `LockContext` class and identity types | | `@cipherstash/stack/client` | Client-safe exports (schema builders and types only - no native FFI) | @@ -763,31 +763,33 @@ depend on `@cipherstash/stack` (they are no longer subpaths of it): | Package | Provides | |-------|-----| | `@cipherstash/stack-drizzle` | EQL v3 Drizzle integration (package root, v3 only): `types` column factories, `createEncryptionOperators`, `extractEncryptionSchema`, `makeEqlV3Column`, `EncryptionOperatorError` | -| `@cipherstash/stack-supabase` | Supabase integration: `encryptedSupabaseV3` (and the legacy v2 `encryptedSupabase`) | +| `@cipherstash/stack-supabase` | Supabase integration (v3 only): `encryptedSupabase` (`encryptedSupabaseV3` is a `@deprecated` alias) | ## Legacy: EQL v2 Before the concrete-domain types above, encrypted columns were declared with chainable capability builders and stored in a single `eql_v2_encrypted` -composite column type. The scalar surface remains supported for existing -deployments, but new work should use EQL v3. Legacy searchable JSON cannot be -emitted by protect-ffi 0.30 and must migrate to v3 `types.Json`: +composite column type. **v2 is now a read path, not an authoring surface**: +`decrypt` / `decryptModel` still read stored v2 payloads, so existing +deployments keep working, but new work must use EQL v3. Legacy searchable JSON +cannot be emitted by protect-ffi 0.30 and must migrate to v3 `types.Json`: - **Client and schema**: `Encryption` from `@cipherstash/stack` with `encryptedColumn("email").equality().freeTextSearch().orderAndRange()` and - the builders from `@cipherstash/stack/schema`. v2 and v3 tables cannot be - mixed in one client. + the builders from `@cipherstash/stack/schema`. These are still exported but + `@deprecated` — do not author new schemas with them. v2 and v3 tables cannot + be mixed in one client. - **Query formatting**: v2 query terms can be rendered as strings with `returnType: 'composite-literal'` / `'escaped-composite-literal'` for string-based APIs. -- **Integrations**: the v2 Supabase surface is `encryptedSupabase`. There is no - v2 Drizzle surface any more — `@cipherstash/stack-drizzle` dropped - `encryptedType` and the v2 operators, so existing v2 Drizzle columns are - read-only: decrypt them through `@cipherstash/stack` and migrate to a v3 - domain. -- **DynamoDB still requires v2**: `encryptedDynamoDB` from - `@cipherstash/stack/dynamodb` works with the v2 API only — v3 support is - tracked in [#657](https://github.com/cipherstash/stack/issues/657). +- **Integrations are v3 only.** `encryptedSupabase` is the EQL v3 factory — + there is no v2 Supabase wrapper any more. Likewise + `@cipherstash/stack-drizzle` dropped `encryptedType` and the v2 operators. + Existing v2 columns reached through either adapter are read-only: decrypt + them through `@cipherstash/stack` and migrate to a v3 domain. +- **DynamoDB writes EQL v3 only.** `encryptedDynamoDB` from + `@cipherstash/stack/dynamodb` encrypts with `types.*` v3 tables; its decrypt + methods still accept a v2 table so previously stored items remain readable. Full v2 documentation lives at [cipherstash.com/docs](https://cipherstash.com/docs). diff --git a/packages/stack/src/encryption/v3.ts b/packages/stack/src/encryption/v3.ts index dc4602e13..070bce612 100644 --- a/packages/stack/src/encryption/v3.ts +++ b/packages/stack/src/encryption/v3.ts @@ -334,8 +334,9 @@ export const EncryptionV3 = Encryption // Single import surface: re-export the v3 `types` namespace + table API + type // helpers so `@cipherstash/stack/v3` provides everything needed to author and -// use a schema. `Encryption` comes along for the same reason — it is the -// current name for what `EncryptionV3` aliases, so authoring a v3 schema and -// building its client should not need a second import specifier. -export { Encryption } +// use a schema. export * from '@/eql/v3' +// `Encryption` comes along for the same reason — it is the current name for +// what `EncryptionV3` aliases, so authoring a v3 schema and building its +// client should not need a second import specifier. +export { Encryption } diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index 430b41952..7cf99621a 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -475,7 +475,7 @@ stash encrypt cutover --table users --column email In one transaction it renames `` → `_plaintext` and `_encrypted` → ``, advances the pending config to `encrypting`, activates it, and appends a `cut_over` event. With a Proxy URL configured (`--proxy-url` or `CIPHERSTASH_PROXY_URL`) it then calls `eql_v2.reload_config()` so Proxy picks up the new shape. -> **After cutover, `` holds ciphertext — the read path is not automatic.** Wire reads through the encryption client (`decryptModel(row, table)` for Drizzle, the `encryptedSupabaseV3` wrapper for Supabase — `encryptedSupabase` on the legacy v2 surface, otherwise `decrypt` / `bulkDecryptModels`) before returning values to callers. Skip this and your read paths hand raw EQL payloads to end users. The integration skill has the exact API. **CipherStash Proxy is the one exception** — it decrypts on the wire, so Proxy users need no application change. The cutover plan written by `stash plan` includes this read-path switch as an explicit step. +> **After cutover, `` holds ciphertext — the read path is not automatic.** Wire reads through the encryption client (`decryptModel(row, table)` for Drizzle, the `encryptedSupabase` wrapper for Supabase, otherwise `decrypt` / `bulkDecryptModels`) before returning values to callers. Skip this and your read paths hand raw EQL payloads to end users. The integration skill has the exact API. **CipherStash Proxy is the one exception** — it decrypts on the wire, so Proxy users need no application change. The cutover plan written by `stash plan` includes this read-path switch as an explicit step. > > **Known gap (v2).** The pending-configuration precondition is satisfied by `stash db push`. SDK-only users (who otherwise never need `db push`) must therefore run it once before `encrypt cutover`. This gap is specific to the legacy v2 path and is not being decoupled — EQL v3 columns sidestep it entirely (no configuration table, no cut-over; see above), which is how [issue #585](https://github.com/cipherstash/stack/issues/585) was resolved when v3 became the default. diff --git a/skills/stash-drizzle/SKILL.md b/skills/stash-drizzle/SKILL.md index 027deb6ab..057280571 100644 --- a/skills/stash-drizzle/SKILL.md +++ b/skills/stash-drizzle/SKILL.md @@ -1,6 +1,6 @@ --- name: stash-drizzle -description: Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle (EQL v3). Covers the types.* encrypted column factories (concrete Postgres domains), auto-encrypting query operators (eq, ne, gt/gte/lt/lte, between, inArray, matches, contains, JSON selector, asc/desc), schema extraction, the EncryptionV3 typed client, database setup with stash eql install, and migrating existing plaintext columns to encrypted. Use when adding encryption to a Drizzle ORM project, defining encrypted Drizzle schemas, or querying encrypted columns with Drizzle. +description: Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle (EQL v3). Covers the types.* encrypted column factories (concrete Postgres domains), auto-encrypting query operators (eq, ne, gt/gte/lt/lte, between, inArray, matches, contains, JSON selector, asc/desc), schema extraction, the Encryption typed client, database setup with stash eql install, and migrating existing plaintext columns to encrypted. Use when adding encryption to a Drizzle ORM project, defining encrypted Drizzle schemas, or querying encrypted columns with Drizzle. --- # CipherStash Stack - Drizzle ORM Integration @@ -125,7 +125,7 @@ Value families: `Integer`/`Smallint`/`Numeric`/`Real`/`Double` (`number`), `Bigi ```typescript import { extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack-drizzle" -import { EncryptionV3 } from "@cipherstash/stack/v3" +import { Encryption } from "@cipherstash/stack/v3" // Convert the Drizzle table definition to a CipherStash v3 schema const usersSchema = extractEncryptionSchema(usersTable) @@ -134,12 +134,12 @@ const usersSchema = extractEncryptionSchema(usersTable) ### 2. Initialize the Encryption Client ```typescript -const encryptionClient = await EncryptionV3({ +const encryptionClient = await Encryption({ schemas: [usersSchema], }) ``` -`EncryptionV3` returns a strongly-typed client: plaintext types are pinned to each column's domain, and query methods only accept queryable columns. +`Encryption` returns a strongly-typed client: plaintext types are pinned to each column's domain, and query methods only accept queryable columns. ### 3. Create Query Operators @@ -464,13 +464,13 @@ Update the encryption client to harvest the encrypted columns from the table: ```typescript // src/encryption/index.ts -import { EncryptionV3 } from '@cipherstash/stack/v3' +import { Encryption } from '@cipherstash/stack/v3' import { extractEncryptionSchema } from '@cipherstash/stack-drizzle' import { users } from '../db/schema' const usersEncryptionSchema = extractEncryptionSchema(users) -export const encryptionClient = await EncryptionV3({ schemas: [usersEncryptionSchema] }) +export const encryptionClient = await Encryption({ schemas: [usersEncryptionSchema] }) ``` Generate the migration with `drizzle-kit generate`. The generated SQL should be a single `ALTER TABLE ... ADD COLUMN email_encrypted public.eql_v3_text_search;`. Apply with `drizzle-kit migrate`. (This requires the EQL v3 SQL to be installed first — see Database Setup.) diff --git a/skills/stash-encryption/SKILL.md b/skills/stash-encryption/SKILL.md index 749c38f22..31f939907 100644 --- a/skills/stash-encryption/SKILL.md +++ b/skills/stash-encryption/SKILL.md @@ -189,7 +189,7 @@ The SDK never logs plaintext data. | `@cipherstash/stack/errors` | `EncryptionErrorTypes`, `StackError`, error subtypes, `getErrorMessage` | | `@cipherstash/stack/types` | All TypeScript types | | `@cipherstash/stack-drizzle` | Drizzle ORM integration for EQL v3 schemas — the package root, EQL v3 only (see the `stash-drizzle` skill) | -| `@cipherstash/stack-supabase` | `encryptedSupabaseV3` wrapper for Supabase (see the `stash-supabase` skill) | +| `@cipherstash/stack-supabase` | `encryptedSupabase` wrapper for Supabase — EQL v3 only (see the `stash-supabase` skill) | | `@cipherstash/stack/wasm-inline` | The **edge** entry — Deno, Bun, Cloudflare Workers, Supabase Edge Functions. Its own `Encryption` factory plus the v3 authoring surface, `EncryptionErrorTypes`, and the WASM build of protect-ffi inlined into the bundle. No native binding, so no bundler externalisation needed. | | `@cipherstash/stack/dynamodb` | `encryptedDynamoDB` — encrypt/write is **EQL v3 only** (`types.*`); decrypt still reads existing v2 items. See the `stash-dynamodb` skill | | `@cipherstash/stack/schema`, `@cipherstash/stack/client`, `@cipherstash/stack/encryption` | Legacy v2 schema builders and client surface — see "Legacy: EQL v2" below | @@ -1006,7 +1006,7 @@ Useful when the backfill needs to run in a worker, on a schedule, or alongside a | Target | Package / entry point | Skill | |---|---|---| | Drizzle ORM | `@cipherstash/stack-drizzle` — v3 column factories (each `types.*` factory emits its domain as the column's SQL type for `drizzle-kit generate`), schema extraction, auto-encrypting operators (`ops.eq`, `ops.matches`, `ops.contains`, `ops.selector`, `ops.asc`, ...) | `stash-drizzle` | -| Supabase | `encryptedSupabaseV3` from `@cipherstash/stack-supabase` — schema-aware query builder (`eq`, `matches`, `contains`, `selectorEq`/`selectorNe`, ...) that works through PostgREST, including as `anon` | `stash-supabase` | +| Supabase | `encryptedSupabase` from `@cipherstash/stack-supabase` — schema-aware query builder (`eq`, `matches`, `contains`, `selectorEq`/`selectorNe`, ...) that works through PostgREST, including as `anon` | `stash-supabase` | | Prisma | `@cipherstash/prisma-next` — searchable field-level encryption for Postgres | — | | DynamoDB | `encryptedDynamoDB` from `@cipherstash/stack/dynamodb` — encrypt is **EQL v3 only**; decrypt still reads existing v2 items | `stash-dynamodb` | @@ -1054,6 +1054,6 @@ const users = encryptedTable("users", { const client = await Encryption({ schemas: [users] }) ``` -The scalar v2 API — `Encryption` plus the `@cipherstash/stack/schema` builders, the v2 Supabase integration (`encryptedSupabase`), and `stash eql install --eql-version 2` — still exists for existing deployments, with two carve-outs. **Drizzle has no v2 surface at all**: `@cipherstash/stack-drizzle` dropped `encryptedType` and the `like`/`ilike` operators, so a v2 Drizzle column is read-only — decrypt it through `@cipherstash/stack` rather than through the adapter. **Legacy v2 `searchableJson()`** cannot be emitted by protect-ffi 0.30 (the selector envelope was removed), so migrate those columns to v3 `types.Json`. Full v2 documentation lives at [cipherstash.com/docs](https://cipherstash.com/docs). Remember: v2 and v3 tables cannot be mixed in one client. (If you are migrating code from the old `@cipherstash/protect` package, its `protect`/`csTable`/`csColumn` names map onto this v2 surface.) +**v2 is a read path now, not an authoring surface.** `decrypt` / `decryptModel` still read stored v2 payloads, and `stash eql install --eql-version 2` still installs the v2 SQL, so existing deployments keep working. What is gone: the **Supabase** and **Drizzle** adapters are EQL v3 only — `encryptedSupabase` is the v3 factory (there is no v2 form), and `@cipherstash/stack-drizzle` dropped `encryptedType` and the `like`/`ilike` operators. A v2 column reached through either adapter is read-only; decrypt it through `@cipherstash/stack` instead. The `@cipherstash/stack/schema` builders (`encryptedColumn(...).equality()`, …) remain exported but are `@deprecated` — do not author new schemas with them. **Legacy v2 `searchableJson()`** cannot be emitted by protect-ffi 0.30 (the selector envelope was removed), so migrate those columns to v3 `types.Json`. Full v2 documentation lives at [cipherstash.com/docs](https://cipherstash.com/docs). Remember: v2 and v3 tables cannot be mixed in one client. (If you are migrating code from the old `@cipherstash/protect` package, its `protect`/`csTable`/`csColumn` names map onto this v2 surface.) > **DynamoDB.** The DynamoDB integration (`encryptedDynamoDB` from `@cipherstash/stack/dynamodb`) now **encrypts EQL v3 only** — author tables with `types.*` from `@cipherstash/stack/eql/v3`. Its decrypt methods still accept a v2 table so previously stored v2 items remain readable. See the `stash-dynamodb` skill. diff --git a/skills/stash-prisma-next/SKILL.md b/skills/stash-prisma-next/SKILL.md index a19bae603..632af947c 100644 --- a/skills/stash-prisma-next/SKILL.md +++ b/skills/stash-prisma-next/SKILL.md @@ -117,7 +117,7 @@ export const db = postgres({ `cipherstashFromStack({ contractJson })` derives the v3 encryption schemas from the contract (one `public.eql_v3_*` domain per column), constructs the -`@cipherstash/stack` `EncryptionV3` client from your `CS_*` env vars or local +`@cipherstash/stack` `Encryption` client from your `CS_*` env vars or local profile, builds the SDK adapter, and returns ready-to-spread `extensions` and `middleware`.